42.27 双向显示

Emacs 可以显示阿拉伯语、波斯语、希伯来语等书写体系的文本,这类文本水平排版的自然顺序为从右至左。 此外,嵌入在从右至左文本中的拉丁字母与数字段会按从左至右显示; 而嵌入在从左至右文本中的从右至左文本段(如程序源代码注释或字符串中的阿拉伯语、希伯来语文本)也会按从右至左正确显示。 我们将这种混合了从左至右与从右至左的文本称为 双向文本(bidirectional text)。 本节介绍编辑与显示双向文本的功能与选项。

文本在 Emacs 缓冲区与字符串中以 逻辑(logical)(或称 阅读(reading))顺序存储,即人类阅读每个字符的顺序。 在从右至左与双向文本中,字符在屏幕上的显示顺序(称为 视觉顺序(visual order))与逻辑顺序不同;字符在屏幕上的位置不会随字符串或缓冲区位置单调递增。 执行这种 双向重排(bidirectional reordering) 时,Emacs 遵循 Unicode 双向算法(又称 UBA),该算法在 Unicode 标准附录 #9 中有详细说明(https://www.unicode.org/reports/tr9/)。 Emacs 实现了符合 Unicode 9.0 标准要求的“完整双向”级别 UBA。 但需注意,当文本方向与段落基础方向相反时,Emacs 对续行的显示方式与 UBA 略有差异,UBA 要求先换行再重排显示。

Variable: bidi-display-reordering

若该缓冲区局部变量的值为非 nil(默认值),Emacs 会对显示执行双向重排。 重排会影响缓冲区文本,以及缓冲区中文本属性与覆盖层属性提供的显示字符串和覆盖层字符串(see 覆盖层属性,see display 属性)。 若值为 nil,Emacs 不会在该缓冲区执行双向重排。

bidi-display-reordering 的默认值控制不由缓冲区直接提供的字符串重排,包括模式行(see 模式行格式)与标题行(see 窗口标题栏)中显示的文本。

Emacs 永远不会重排单字节缓冲区的文本,即使缓冲区中 bidi-display-reordering 为非 nil。 原因是单字节缓冲区存储原始字节而非字符,缺少重排所需的方向属性。 因此,判断缓冲区文本是否会被重排显示,不能只检查 bidi-display-reordering。 正确判断方式如下:

 (if (and enable-multibyte-characters
          bidi-display-reordering)
     ;; Buffer is being reordered for display
   )

但单字节显示字符串与覆盖层字符串,在其父缓冲区启用重排时 被重排。 因为纯 ASCII 字符串在 Emacs 中以单字节字符串存储。 若单字节显示或覆盖层字符串包含非 ASCII 字符,这些字符默认按从左至右方向处理。

display 文本属性、带字符串类型 display 属性的覆盖层,以及其他替换缓冲区文本的属性覆盖的文本,在重排显示时会被视为一个整体。 即被这些属性覆盖的整块文本会一起重排。 此外,Emacs 会忽略该文本块中字符的双向属性,将其视为单个字符 U+FFFC(即 对象替换字符(Object Replacement Character))处理。 这意味着对部分文本添加显示属性可能改变周围文本的重排方式。 为避免这种意外效果,应始终将这类属性应用在与周围文本方向一致的文本上。

每段双向文本都有一个 基础方向(base direction),要么从右至左,要么从左至右。 从左至右的段落从窗口左边缘开始显示,文本到达右边缘时截断或换行。 从右至左的段落从右边缘开始显示,在左边缘换行或截断。

在 Emacs 的 UBA 实现中,段落起止的精确判定由以下两个缓冲区局部变量决定(注意 paragraph-startparagraph-separate 对此无影响)。 默认两者均为 nil,段落由空行分隔,即由零个或多个空白字符加换行符组成的行。

Variable: bidi-paragraph-start-re

若非 nil,该变量值应为一个正则表达式,匹配作为段落起始或分隔的行。 正则表达式始终在换行后匹配,因此最好以 "^" 锚定开头。

Variable: bidi-paragraph-separate-re

若非 nil,该变量值应为一个正则表达式,匹配分隔两个段落的行。 正则表达式始终在换行后匹配,因此最好以 "^" 锚定开头。

若修改这两个变量中的任意一个,通常应同时修改两者以保证段落定义一致。 例如,若希望每一行都作为双向重排的新段落,可将两个变量均设为 "^"

默认情况下,Emacs 根据段落开头文本确定其基础方向。 确定基础方向的具体方法由 UBA 规定;简单来说,段落中第一个具有显式方向的字符决定段落基础方向。 但某些缓冲区需要强制指定段落基础方向。 例如,程序源代码缓冲区应强制所有段落按从左至右显示。 可使用下列变量实现:

User Option: bidi-paragraph-direction

若该缓冲区局部变量的值为符号 right-to-leftleft-to-right,缓冲区中所有段落均使用指定方向。 其他值等价于 nil(默认值),表示根据内容动态确定段落基础方向。

程序源代码模式应将其设为 left-to-right。 编程模式默认已做此设置,因此继承自编程模式的模式无需显式设置(see 基础主模式)。

Function: current-bidi-paragraph-direction &optional buffer

该函数返回指定 buffer 中点所在位置的段落方向。 返回值为符号,要么是 left-to-right,要么是 right-to-left。 若 buffer 省略或为 nil,默认为当前缓冲区。 若变量 bidi-paragraph-direction 的缓冲区局部值非 nil,返回值与其一致; 否则返回值为 Emacs 动态确定的段落方向。 对于 bidi-display-reorderingnil 的缓冲区以及单字节缓冲区,该函数始终返回 left-to-right

有时需要按严格视觉顺序移动光标,即在当前屏幕位置的左侧或右侧移动。 Emacs 提供了原语实现该功能。

Function: move-point-visually direction

该函数将当前选中窗口的光标移至屏幕上紧邻其右侧或左侧的缓冲区位置。 若 direction 为正,光标向右移动一个屏幕位置;否则向左移动一个屏幕位置。 注意,根据周围双向上下文,这可能导致光标在缓冲区中移动多个位置。 若在屏幕行末尾调用,函数会根据 direction 将光标移至下一行或上一行的最右侧或最左侧屏幕位置。

函数返回新的缓冲区位置作为结果。

当两段包含双向内容的字符串在缓冲区中相邻,或通过程序拼接为一个字符串时,双向重排可能产生意外且糟糕的效果。 典型问题场景如缓冲区菜单模式或 Rmail 摘要模式,这类模式由空白或标点分隔的文本字段序列组成。 由于用作分隔符的标点具有 弱方向性(weak directionality),会继承周围文本的方向。 结果是,跟随在双向内容字段后的数字字段可能显示在前一字段左侧,破坏预期布局。 可通过以下几种方式避免该问题:

Function: bidi-string-mark-left-to-right string

该函数返回其参数 string(可能已修改),使结果可安全地与另一字符串拼接或在缓冲区中相邻显示,而不破坏两段文本的相对布局。 若该函数返回的字符串在从左至右段落中显示,始终会出现在后续文本左侧。 函数通过检查参数字符实现:若其中任何字符可能导致显示重排,则在字符串末尾添加 LRM 字符。 添加的 LRM 字符会被设为不可见,即赋予其 invisible 文本属性为 t(see 不可见文本)。

重排算法使用字符的 bidi-class 属性中保存的双向属性(see 字符属性)。 Lisp 程序可通过调用 put-char-code-property 函数修改这些属性。 但这需要深入理解 UBA,因此不推荐使用。 对字符双向属性的任何修改均为全局生效:会影响所有 Emacs 框架与窗口。

类似地,mirroring 属性用于在重排文本中显示合适的镜像字符。 Lisp 程序可修改该属性影响镜像显示。同样,此类修改会影响整个 Emacs 显示。

可通过插入特殊方向控制字符覆盖字符的双向属性: 从左至右覆盖(LRO)与从右至左覆盖(RLO)。 RLO 与后续换行符或方向格式弹出(PDF)控制字符之间的所有字符(以先出现者为准),都会按强从右至左字符显示,即在显示时反转顺序。 类似地,LROPDF 或换行符之间的字符会按强从左至右显示,即使本身是强从右至左字符也不会反转。

这类覆盖在需要让部分文本不受重排算法影响、直接控制显示顺序时非常有用。 但也可被用于恶意目的,即 钓鱼攻击(phishing)。 具体来说,网页上的 URL 或邮件中的链接可被篡改,使其视觉外观无法辨认或看似为常见安全地址,而浏览器按逻辑顺序解析的真实地址却完全不同。

Emacs 提供原语,应用可用于检测文本中被强制修改方向属性的情况,即使从左至右字符显示为从右至左,或反之。

Function: bidi-find-overridden-directionality from to &optional object

该函数检查指定 object 中从 from(含)到 to(不含)的文本, 返回第一个找到的、方向属性被强制改为从右至左的强从左至右字符位置, 或方向属性被强制改为从左至右的强从右至左字符位置。 若在指定文本区域未找到此类字符,返回 nil

可选参数 object 指定要搜索的文本,默认为当前缓冲区。 若 objectnil,可以是其他缓冲区、字符串或窗口。 若为字符串,函数搜索该字符串;若为窗口,搜索该窗口显示的缓冲区。 若要检查的缓冲区显示在某个窗口中,建议通过该窗口指定,而非直接传入缓冲区。 因为告知函数窗口信息可正确处理窗口专属覆盖层,若缓冲区中部分文本被覆盖层覆盖,这会影响函数结果。

当包含混合从右至左与从左至左字符及双向控制符的文本被复制到其他位置时,其视觉外观可能改变,也会影响目标位置周围文本的显示。 原因是 UBA 规定的双向文本重排具有复杂的上下文依赖效果,既影响被复制文本,也影响目标位置的周围文本。

有时 Lisp 程序需要在目标位置精确保留被复制文本及其周围文本的视觉外观。 Lisp 程序可使用下列函数实现该效果。

Function: buffer-substring-with-bidi-context start end &optional no-properties

该函数功能类似 buffer-substring(see 查看缓冲区内容), 但会在复制文本前后添加必要的双向方向控制字符,以保证文本插入到其他位置时视觉外观不变。 可选参数 no-properties 若非 nil,表示移除复制文本的文本属性。