CJK 排版
CJK 文本对断行器而言与拉丁文本有一个根本区别:没有可断的空格。任意两个 CJK 字符之间都是潜在断点,因此真正的工作不是找出哪里能断,而是删掉哪里不能断。这就是模型里的禁则部分,它还有两个同伴:全角标点在行缘能做什么——Emacs 无法压缩字形——以及 512MB 这类紧凑「数字+单位」token 怎样保持完整。
本章按 EKP 的实现讲这三个机制。第一个机制背后的断点许可模型见断行原理;这些机制用到的每个选项都是用户选项,汇总在参数参考一章。
EKP 对每个字符分类,并把分类保留在盒子的两端,因此收尾标点与开启标点无论出现在哪里都能被认出来。末字符是收尾标点的盒子不得起行;首字符是开启标点的盒子不得收尾。规则一方面用字符类别覆盖全角标点,一方面用两个固定集合覆盖半角标点:整盒只由行首禁止标点(, ; : ! ? 及其同类)组成的盒子不得起行,整盒只由行尾禁止标点(开括号与开引号)组成的盒子不得收尾。「整盒只由」这一点很重要:仅仅以句点开头的词,比如 .emacs,保留起行的自由。
标点还贴身于它的内容:开启标点之后、收尾标点之前都没有胶,而且那里的间隙是刚性的,因此这条禁止不是优化器可以付费买下的收费,而是断点根本不存在——行会直接跨过它继续延伸。
ekp-kinsoku-profile 选择规则集:
| 取值 | 禁止什么 |
|---|---|
common |
收尾标点起行、开启标点收尾、孤立的半角标点 |
zh |
与 common 相同的规则;当前实现里两个取值行为完全一致 |
ja |
通用规则再加上不可更改的内置日文集合:小假名、片假名音标扩展、长音符与叠字符 |
custom |
通用规则再加上 ekp-cjk-no-line-start-extra 与 ekp-cjk-no-line-end-extra 中的字符 |
off |
不做任何禁则:收尾标点可以起行,开启标点可以收尾 |
custom 方案把两个 extra 字符串按字符读取,而不是当模式匹配:盒子不得以 ekp-cjk-no-line-start-extra 中任一字符开头,也不得以 ekp-cjk-no-line-end-extra 中任一字符结尾。两者默认都是空字符串,所以不带 extra 的 custom 与 common 行为一致。日文附加集不受 ekp-cjk-no-line-start-extra 影响;off 只关掉禁止本身——标点周围的贴身胶是盒子的属性,与方案无关。
改动方案或任一 extra 字符串都会改变段落分析键,因此已排好的段落在下一次排版调用时会按新值重新解析。
示例。 对日文句子 きょうのてんきはとてもいいので、さんぽにいこうとおもいます。,common 方案在较窄宽度下允许小假名 ょ 与 っ 起行,而 ja 在 8 到 110 像素的全部宽度下都不产生这样的行。设为 off 时全角 。 可以在八像素或十四像素处起行,common 从不允许。common 下孤立的半角 , 出现在 CJK 文本里也不会起行,而以句点开头的拉丁词可以。
传统 CJK 排版靠在行缘压缩标点字形。Emacs 无法缩小字形的步进,因此 EKP 渲染不了行中压缩;左缘同样无法悬挂,因为文字不能画到行原点之前。剩下的就是右缘释放:一行可以让末字形的一部分悬挂到排版边缘之外,为它前面的文字腾出空间。
ekp-protrusion 打开这一行为,ekp-protrusion-ratios 给出各类行末字形可悬挂的宽度比例:
| 类别 | 行末字形 | 默认比例 |
|---|---|---|
cjk-close |
全角收尾标点,如 。、」 |
0.5 |
latin-close |
恰好收尾的行首禁止集字符,如句号、逗号、引号 | 0.5 |
hyphen |
连字符断点插入的软连字符 | 1.0 |
释放量是比例乘字形实测宽后向下取整到整像素;全角收尾标点取 0.5 时正好只悬挂该字形的空白那一半,视觉上等价于行尾压缩。引擎把释放的像素算作可用宽度,所以悬挂行可以比普通最大宽略宽而仍然合法,字符串接口报告的行宽也会比目标多出释放量。选项开启时,buffer 集成会在排版中预留这段悬挂宽度,使它前面的文字不会撞上窗口边缘。
选项及其比例表都是段落键的一部分,因此开关悬挂或改动比例都会让段落重新解析;悬挂关闭时没有任何释放量,每行都遵守最大宽。
示例。 六句 四字一句。 在 20 像素下,悬挂关闭时每行实测正好 20 像素。用默认比例时每行实测 21——全角 。 悬挂出两个像素中的一个。把 cjk-close 比例升到 1.0 后每行变为 22,降到 0 又回到 20。40 像素下的一次连字符断行,默认 hyphen 比例 1.0 时行宽 41,悬挂关闭时 40。
紧凑的「数字+单位」token 按形状识别,而不是按词表:token 以数字开头,后缀之前的部分是整数或用 .、, 书写的小数,且 token 以 ekp-number-unit-suffixes 中的某项收尾。默认表覆盖百分号与千分号、摄氏与华氏度、像素与字体长度、时间单位、频率、字节单位以及公制质量与长度单位。匹配是精确且区分大小写的,所以 512MB 命中,512 MB 是两个普通 token,MB512 不是单位 token。应用替换这张表,就是教 EKP 认识自己的单位的方式。
该类别交给 ekp-token-break-policies,其中 number-unit 的默认策略是 no-break。token 就此成为自动刚性原子:内部既不断行也不断字,也不做内部分割——识别出的形状保持完整。宽过行宽的原子按 ekp-overlong-token-policy 处理:默认的 emergency 把它降级为 no-hyphen,overflow 保持刚性,natural 把段落交给外围的 Emacs 换行处理并原样返回。对紧凑的单位 token 而言,降级之后也没有合法内部边界可用,因此 token 仍然完整地占一行溢出——这正是断行原理里不可断原子的最终去向。
示例。 在 30 像素下,aa 123456789012345678901234567890MB bb cc dd ee ff 在 emergency 与 overflow 下把超宽单位 token 放在一行 34 像素的溢出行里,其余内容另起一行;在 natural 下整串原样返回为一行,交给调用方换行。若该单位不在 ekp-number-unit-suffixes 里,token 就是普通拉丁式 token,同样的超宽规则照常适用于它。
没有文本会因为属于 CJK、日文或混排而被拒绝:本章的机制改变的是存在哪些断点以及行缘有多少余量,任何输入依然都能得到完整方案。这里的失败模式是选项值畸形,因为这些选项在设定时不做校验。ekp-number-unit-suffixes 取值不是序列时,下一次排版报 wrong-type-argument,数据点名 sequencep;ekp-protrusion-ratios 取值不是 alist 时报 wrong-type-argument,数据点名 listp,而键不是预期符号的表只是查不中、什么都不释放。未知的 ekp-kinsoku-profile 取值回退到通用规则,非字符串的 extra 集被忽略。这些选项都是段落分析键的输入,所以修正取值就是全部的恢复手段:下一次排版调用就用修正后的值。