双向文本 (BiDi)
处理从左到右 (LTR) 和从右到左 (RTL) 文本混合的技术,在包含阿拉伯语和希伯来语的多语言文本中必需。
双向文本 (BiDi: Bidirectional Text) 是处理从左到右 (LTR: Left-to-Right) 书写的语言与从右到左 (RTL: Right-to-Left) 书写的语言在同一文本中混合的技术。从右向左书写的文字体系包括阿拉伯文、希伯来文、Thaana 文字 (迪维希语)、叙利亚文等,其中阿拉伯文不仅用于阿拉伯语,也用于波斯语和乌尔都语。对于面向全球用户的网站,BiDi 支持是不可回避的课题。
Unicode 双向算法 (UBA: Unicode Bidirectional Algorithm) 自动判定文本中每个字符的方向性,并确定正确的显示顺序。规范定义在 Unicode Standard Annex #9"Unicode Bidirectional Algorithm"中。每个字符都被赋予了方向类型,分为"强"类型 (拉丁字母为 L,希伯来文为 R,阿拉伯文、Thaana 文字、叙利亚文为 AL)、"弱"类型 (欧洲数字与阿拉伯数字、数字分隔符) 和"中性"类型 (空白及其他符号)。即使在 RTL 的文字体系中数字也是从左向右书写,因此阿拉伯语和希伯来语的句子本质上就是双向的。
弱类型与中性类型字符的处理会随周围语境变化,因此在数字与符号混杂的位置容易出现排版错乱。分隔符只有在两侧都被数字夹住时才会被当作数字的一部分,否则就成为中性字符,被周围的 RTL 拉动而改变位置。在 RTL 的句子中写下"123, 456, 789",各个数字内部的顺序仍然保持从左到右,但这三个数字会按从右到左排列,逗号则附在每个数字的左侧,显示成"789 ,456 ,123"这样。电话号码开头的国家代码 + 也没有被数字夹住,同样成为中性字符,在显示上会跑到号码的另一侧。把含有数字的字符串直接放进 RTL 句子中之所以危险,原因就在于这种行为。
在 HTML 中,用 dir="rtl" 属性显式指定方向,用 <bdo> (Bidirectional Override) 元素强制排列顺序。对于用户输入的姓名或搜索词这类事先无法确定方向的字符串,惯用做法是用 <bdi> 元素包裹,或加上 dir="auto",让浏览器根据最先出现的强方向字符来决定方向。若不加包裹,紧跟在 RTL 姓名之后的标点或括号会被拉向姓名一侧,整句的排列就会错乱。CSS 的 direction 属性和逻辑属性 (margin-inline-start、padding-inline-end 等) 对 BiDi 支持同样重要。用逻辑属性代替物理属性 (margin-left),就能在 LTR/RTL 切换时自动适配。
在无法使用标记的场合 (纯文本通知、CSV、提交信息等),可以用 Unicode 的方向控制字符实现同样的控制。用于隔离的控制字符是 U+2066 LRI (相当于 dir="ltr")、U+2067 RLI (相当于 dir="rtl")、U+2068 FSI (相当于 dir="auto"),三者都以 U+2069 PDI 闭合。Unicode 标准推荐使用隔离类控制字符,而不是较旧的嵌入类 U+202A LRE / U+202B RLE (以 U+202C PDF 闭合)。不过控制字符不会跨越段落边界,因此整份文档的默认方向仍需在标记一侧指定。判断标准很简单:能用标记就用属性,不能用标记就用控制字符。
BiDi 文本还存在安全方面的隐患。2021 年报告的 Trojan Source 攻击展示了这样一种手法:滥用 Unicode 的方向控制字符 (RLO、LRI 等),使源代码的外观与实际执行顺序不一致。覆盖类控制字符 (U+202D LRO、U+202E RLO) 与隔离类控制字符都是零宽度、不会出现在画面上,因此评审时只相信肉眼所见就无法察觉。主流代码编辑器和差异显示工具都具备将这些不可见字符可视化或发出警告的功能。在接收源代码或文件名的机制中,加入机械化检测方向控制字符混入的检查会更安全。
在字符计数方面,BiDi 文本的视觉字符数与内部字符数可能并不一致。方向控制字符 (U+200E LRM、U+200F RLM,以及 U+2066 至 U+2069 的隔离类控制字符等) 是零宽度的,不会显示在画面上,但会被计入字符数。隔离类控制字符开启与闭合成对,一对就是 2 个字符,因此在句中插入并包裹 5 个姓名,仅此一项就会增加 10 个字符。若发帖上限是 140 个字符,就会出现看上去只写了 130 个字符却已经触及上限的现象。实务上的折中做法是:判断上限之前先统计去掉方向控制字符后的字符数,而保存和显示时则原样保留。一旦把它们删除,显示的排列就会错乱,所以要避免一律清除。