连字 (Ligature)

将两个或多个字符组合为一个字形显示的排版技法。常见例子包括 fi、fl 和 ff。

连字 (Ligature) 是一种将两个或多个字符结合、作为单个字形 (glyph) 显示的排版技法。在拉丁字母中,fi、fl、ff、ffi 是代表性的连字,自活字印刷时代起就被用作避免字符之间物理碰撞、实现优美排版的手段。现代的数字字体也继承了这一传统,作为 OpenType 字体的功能被广泛实现。

连字的机制定义在字体文件内的 GSUB (Glyph Substitution) 表中。当文本渲染引擎检测到特定的字符排列时,就会用结合后的字形代替各自单独的字形来绘制。例如"f"和"i"相邻时,f 的上升部与 i 的点会相互干扰,因此替换为专用的连字字形以保持视觉上的和谐。

启动替换的功能并非只有一种。在 OpenType 的功能标签中,像 fi、ffi 这样的标准连字对应 liga,依上下文施加的标准连字对应 clig,字体设计师准备的装饰性连字对应 dlig,旧排版样式的连字对应 hlig,而书写体系上必需的结合则分配给 rlig。它们在实现上都是 GSUB 的查找类型 4 (连字替换),把多个字形的排列映射为 1 个字形。若把"连字就是字体的 liga 功能"一概而论,就会漏掉后文所述编程字体那样用别的功能实现的连字。

近年来,编程字体中的编码连字备受关注。在 Fira Code、JetBrains Mono、Cascadia Code 等字体中,会把 != 显示为 ≠、=> 显示为 ⇒、<= 显示为 ≤。由此运算符的辨识度得到提升,代码的可读性也随之提高。不过,编码连字终究只是显示层面的转换,源代码本身仍以原来的字符串保存。

实务中容易让人困惑的是,编码连字并非用 liga,而是用 calt (上下文相关字形) 来实现的这一点。Fira Code 在官方的导入步骤中,介绍的是通过 font-feature-settings: "calt" 或 font-variant-ligatures: contextual 来启用。因此即使只指定 no-common-ligatures,箭头和比较运算符的连字也不会消失;想关掉时需要一并写上 no-contextual,或者指定 none。如果编辑器一侧存在连字的开关设置,那么优先生效的是那一侧而不是 CSS。

在 CSS 中可以用 font-variant-ligatures 属性控制连字的启用与禁用。取值分为 4 个系统:common-ligatures / no-common-ligatures 对应 liga 与 clig,discretionary-ligatures 对应 dlig,historical-ligatures 对应 hlig,contextual 对应 calt。这里想要掌握的是默认值的行为。在作为初始值的 normal 下,一般的连字与上下文相关字形是启用的,装饰性连字与古典连字是禁用的。也就是说,只要指定了带有连字的字体,fi 和 ffi 什么都不写也会被应用,需要动手写 CSS 的场合是"想加上装饰性连字时"或"想停用连字时"。指定 none 则会禁用全部 4 个系统。

作为常见的误解,存在把连字与字距调整 (Kerning) 混同的情况。字距调整是调整字符间距离的技法,而连字是把多个字符替换为 1 个字形的技法。两者在追求排版之美这一点上相通,但机制根本不同。此外,像阿拉伯文那样字符的连接已被编入书写体系基本规则的情况下,连字不是装饰而是必需的功能。代表性的例子是 lam 后接 alef 时必定变为连接形,OpenType 把它登记为 rlig (Required Ligatures),叙利亚文也同样处理。规范指出一般不应把这一功能的控制开放给用户,因此它不是可以凭外观喜好去关闭的对象。

从字符计数的观点看,连字在显示上被绘制为 1 个字形,但在内部仍作为原来的多个字符处理。例如 fi 连字看起来是 1 个字符,但作为字符数会被计为 2 个字符。在文本编辑器中移动光标时,一般也能在连字的内部逐字符移动。换了字体而字符数不变,是因为这种替换只发生在绘制阶段。

不过存在例外。Unicode 中有把连字本身作为 1 个字符收录的码位,例如 fi (U+FB01 LATIN SMALL LIGATURE FI) 和 ffl (U+FB04),它们与字体的功能无关,会被数作 1 个字符。这类字符有时会混入从旧 PDF 或文字处理文档复制来的文章中,看起来相同,却与把 f 和 i 并排的 2 个字符是不同的东西。这一差别会通过规范化浮现出来。经过兼容规范化 (NFKC) 后,U+FB01 会展开为 f 和 i 这 2 个字符,U+FB04 则增加为 3 个字符。另一方面,合成规范化 (NFC) 下不会发生变化,因此在以 NFC 为前提的系统中仍会保持为 1 个字符。当字符数与预想不符时,请先确认输入字符串中是否混有这些码位,以及经过的是哪种规范化。另外,ß 和 Œ 虽然来历上是连字,但被当作独立的字符处理,在 NFKC 下也不会被分解。

分享这篇文章