断行原理
EKP 为整个段落一次性选定全部断行位置,而不是逐行填满。它采用 Knuth–Plass 模型:段落是一串由胶连接的盒子,每个合法断点携带一个罚分,一个断行方案的优劣由各行代价之和决定。因为选择是针对整段做的,一个能填好本行却逼出难看下一行的断点,会输给整体更均匀的方案;贪心填行无法收回先前的决定,而整段搜索可以。
本章按 EKP 的实现讲这套模型:文字怎样变成盒子与胶,一行怎样用劣度、适应度等级与代价计分,松紧度改变了什么,以及两遍应急策略怎样让任何输入都能排出结果。本章自己的接口是下面的显式间距参数;它点名的各个数值与策略全部是用户选项,汇总在参数参考一章。结果字符串与背后的结构化方案见编程接口一章。
段落是一串盒子(box)。盒子是断行永不拆开的最小单位:一个拉丁词、一个连同贴身标点的 CJK 字符,或一段空白。盒子的类型与边缘字符决定两个盒子之间能放什么、断点是否合法,因此收尾的 。 会留在它收束的那一行,开引号会跟着它开启的文字。
盒子之间是胶(glue)。胶没有单一宽度:它有一个理想宽、一个可拉伸量和一个可压缩量,单位都是像素。EKP 给每个间隙归入三类胶之一——两个拉丁词之间的拉丁词间胶、拉丁与 CJK 之间的混合胶、CJK 字符与标点周围的 CJK 胶——而在刚性接合处不加胶:那里的断行要么不可能,要么由字符自身提供间距。描述这三类胶的九个像素值就是本模型的间距参数。
罚分(penalty)是对断点收取的代价。普通断点不收罚分,连字符断点收 ekp-hyphen-penalty。有了这种收取,一条合法但略被拉伸的行可以赢过一条带连字符的行。
一行的劣度(badness)衡量它的胶合宽度离目标宽度差多远:胶处于理想宽时劣度为 0,一行相对自身胶能力拉伸或压缩得越多,劣度越大,直到一个被当作无穷的封顶值。同一个比值还给这一行定出适应度等级(fitness class)——紧、适中、松、极松——而代价(demerits)把劣度、断点罚分和两项一致性收费合在一起:相邻行的适应度等级相差超过一级,以及连续的连字符行。公式见为一行计分。
松紧度(looseness)是对最优行数的偏移请求。取 0 时段落采用代价最低的方案;取正或负值表示要求更多或更少的行,EKP 取行数与请求最接近者。
有些文字根本没有普通方案:一个词可能宽过行宽,一段刚性内容可能拒绝拉伸。两遍应急策略同时处理这两种情况:先跑严格一遍,只有当段落终点不可达时,第二遍才给欠满的候选加一笔有限的背景拉伸,并用同样的劣度、适应度与代价公式计分。最后一条可达性规则会在拒绝某个溢出候选就等于掐灭通往某断点的最后一条路径时,保住该候选。于是任何输入都能得到完整方案。
三类胶背后的九个值有它们自己的接口。ekp-param-set 显式写入这些值,ekp-param-reset 回到按每个段落的字体自动派生。
Function: ekp-param-set (lws-i lws-+ lws-- mws-i mws-+ mws-- cws-i cws-+ cws--)
Section titled “Function: ekp-param-set (lws-i lws-+ lws-- mws-i mws-+ mws-- cws-i cws-+ cws--)”显式设定全部间距参数;持续到 ekp-param-reset 为止。
参数。 九个像素值依次为 LWS-I、LWS-+、LWS--、MWS-I、MWS-+、MWS--、CWS-I、CWS-+、CWS--:拉丁(LWS)、混合(MWS)与 CJK(CWS)词间胶各自的理想宽、拉伸量(+)与压缩量(−)。
语义。 这些值取代按字符串自动派生的默认值,对之后每一次排版、每个缓冲区都生效,直到 ekp-param-reset。每一类各自保留自己的拉伸与压缩额度;派生的最大与最小宽度会重算,段落也按新值重新解析,而不是继续用旧值算出的缓存。零是合法值:某类可以取理想宽 0、不拉伸也不压缩,让它的间隙变成刚性。
返回。 t。
错误。 任一位为 nil 时 error→ error,消息为 "All spacing parameters must be non-nil"。任一位不是数字时 error→ wrong-type-argument,数据点名 number-or-marker-p。被拒绝的调用不会留下显式参数:下一次排版仍按段落字体派生默认值,失败的数值到不了任何行。
示例。
(ekp-param-set 4 8 2 3 6 2 0 4 0)(length (split-string (ekp-pixel-justify "one two three four five six seven eight nine ten" 60) "\n")) ⇒ 2(ekp-param-reset)(length (split-string (ekp-pixel-justify "one two three four five six seven eight nine ten" 60) "\n")) ⇒ 1参见。 ekp-param-reset。
Function: ekp-param-reset ()
Section titled “Function: ekp-param-reset ()”清除显式间距参数;默认值重新按每个字符串派生。
语义。 九个值与「曾显式设定过」的记录一并清除。下一次排版某个段落时,它的间距从该段落的字体派生,和从未调用过 ekp-param-set 的会话完全一样。已经排好的段落保留原有行,直到下一次排版调用;重置本身不会重排任何内容。
返回。 nil。
错误。 无。本函数不接收参数,因此没有可拒绝的输入。
示例。
(ekp-param-set 7 3 2 5 2 1 0 2 0)(ekp-param-reset)(length (split-string (ekp-pixel-justify "one two three four five six seven eight nine ten" 60) "\n")) ⇒ 1参见。 ekp-param-set。
候选行横跨一段盒子,有四个宽度:全部胶取理想值时的理想宽、全部胶压到下限的最小宽、全部胶拉到上限的最大宽,以及本次排版的目标宽。当 最小 ≤ 目标 ≤ 最大 时该行是合法断点;段落最后一行欠满时同样合法,即 理想 ≤ 目标。行尾悬挂标点会在右缘放出末字形的一部分,因此带悬挂的行可以比普通最大宽略宽而仍然合法。
调整量是 目标 − 理想。调整量为零时劣度为 0;本行的弹性为零或负时劣度为 10000,即被当作无穷的值;否则
badness = min(10000, 100 × |adjustment / flexibility|³)其中弹性对拉伸的行是各行间隙拉伸量之和,对压缩的行是各间隙压缩量之和。比值 调整量 / 弹性 还给本行定出适应度等级:
| 比值 | 等级 |
|---|---|
| 低于 −0.5 | 紧 |
| −0.5 到 0.5 | 适中 |
| 0.5 到 1.0 | 松 |
| 1.0 及以上 | 极松 |
一行的代价在基础代价上加上两项一致性收费:
demerits = (ekp-line-penalty + badness)² + ekp-hyphen-penalty² ; 只在连字符断点 + ekp-adjacent-fitness-penalty ; 相邻等级相差超过一级时 + ekp-consecutive-hyphen-penalty × run² ; 只在连字符行延续时最后一项里的 run 统计到本断点为止(含本行)连续带连字符的行数,因此带连字符的第二行比第一行罚得更重,第三行又重于第二行。相邻适应度收费是平坦常量而非倍数,两项收费一律相加,从不取负。
有两类行偏离通式:
- 单盒行——只容纳一个不可断盒子的行,例如没有断字点的词——在严格一遍里弹性固定为 1,劣度直接来自它差了多少像素。在应急一遍里,正的调整量改用应急拉伸来计分。
- 最后一行收取
(ekp-line-penalty + short-badness)²,其中short-badness在填充比理想 / 目标低于ekp-last-line-min-ratio时为ekp-last-line-short-penalty × (1 − 填充比),否则为 0。最后一行免收适应度收费,取适中等级,也从不支付连字符罚分。
搜索从左到右扫过断点。每个到达的断点保留「到此为止最省的方案」的代价,以及产生它的前一个断点;新候选以更少代价到达同一点时,取代旧的记录。断行被禁止的位置——无论是禁则规则、位于不可断原子内部,还是无断行字符把邻居连在一起——根本不成候选,搜索会继续延长本行跨过它。到达段落终点后,沿记录的前驱回溯,就得到选定的断点。
严格一遍只接受合法断点。如果段落终点不可达,应急一遍带着给欠满候选最大宽加上的有限背景拉伸重跑;默认拉伸量是当前字体 M 宽的三倍。同一遍还实现了对溢出候选的可达性规则:当拒绝某个候选会让一个既没有存活非溢出候选、又再无其它路径的断点失去路径时,装入最优的临时路径,适应度记紧、不追加任何代价。这条规则只在普通路径已经丢失时触发,从不与普通路径竞争,也不看 CJK、单位或 token 类别。正是它保证任何输入都有完整方案,包括宽过目标宽度而不可断的词。
ekp-looseness 与 ekp-parshape 会把搜索切换成按位置与行数展开的二维动态规划。该规划先找出代价最低的行数,再选出与 ekp-looseness 所要求的偏移最接近的行数,代价更低者打破平局。这两项特性只由 Emacs Lisp 引擎计算:它们任一被设置时,即使原生模块已加载且 ekp-use-c-module 非 nil,也不会走原生模块。普通的一维搜索两个引擎都有,且结果一致;原生模块只是算得更快,不会算出不同答案。
本章模型不会拒绝任何段落。排版没有失败路径:严格一遍、应急一遍与可达性规则共同保证任何盒子序列都有方案,不可断内容被如实记为一行溢出,而不是报错。宽度参数不合法时,由接收它的入口报错,「编程接口」一章有所述。
间距参数接口是本章唯一会报错的地方。ekp-param-set 对 nil 参数报普通错误,对非数字报 wrong-type-argument;两种情况下,被拒绝的调用都不会留下显式参数,下一次排版照常派生自动默认值。恢复方式是用九个数字重新调用 ekp-param-set,或调用 ekp-param-reset 把自动默认值确立为预期状态。