← 博客

控制台中的从右到左文本

Text.js 实现了 Unicode 双向算法,通过全部 770,241 个 BidiTest.txt 测试用例。希伯来语和阿拉伯语按视觉顺序渲染,支持阿拉伯字形塑形,并与单元格网格对齐。

Adam

Text.js 现在能在与其他内容相同的单元格网格上,按正确顺序渲染希伯来文、阿拉伯文、波斯文及其他从右到左书写的文字,并连接阿拉伯字母。

import { Box, Text } from "@textjs/react";export default function Page() {  return (    <Box flexDirection="column" padding={1}>      <Text>שלום עולם! זהו טקסט בעברית.</Text>      <Text>مرحبا بالعالم! هذا نص عربي.</Text>      <Text>Order #1234 (<Text color="accent">שלום</Text>) total: 25.50 ₪</Text>      <Text>user <Text dir="auto" color="success">שלום!</Text> replied 3 times</Text>    </Box>  );}

每个段落根据第一个强方向字符确定基础方向,与 HTML 中的 dir="auto" 一样,从右到左的段落右对齐。在嵌套的 <Text> 上,dir 像 <bdi> 一样隔离 span,因此方向未知的用户名不会改变周围文本的顺序。

算法

从右到左文本支持在 @textjs/core 中从头实现了 Unicode 双向算法(UAX #9,Unicode 18.0)。算法为每个段落解析嵌入级别,涵盖显式嵌入与隔离、弱类型与中性类型以及成对括号。随后,算法对换行后的每一行单独重排,并在从右到左的级别上替换镜像字符,因此希伯来文中的 (abc) 仍显示为 (abc)。

Unicode 为该算法发布了两个一致性测试文件。两者均以 gzip 压缩格式提交到仓库,测试套件会运行其中的每个用例:

  • BidiTest.txt 以双向类别序列列出输入,并为每项指定适用的段落方向。Text.js 通过了全部 770,241 个用例。
  • BidiCharacterTest.txt 列出真实字符串及其预期级别和顺序。Text.js 通过了全部 91,707 个用例。
for (const [bit, dir] of [[1, -1], [2, 0], [4, 1]] as const) {  if ((bitset & bit) === 0) continue;  cases++;  const r = run(types, null, dir);  const gotLevels = fmtLevels(r.levels);  const gotOrder = r.order.join(" ");  if (gotLevels !== levels || gotOrder !== reorder) {    if (failures.length < 20) failures.push(`${input.trim()} [dir ${dir}]: levels ${gotLevels} (want ${levels}), order ${gotOrder} (want ${reorder})`);    else failures.push("");  }}

从逻辑顺序到单元格

一行从右到左的文本在进入控制台前,会经过四个步骤:

  1. 按逻辑顺序换行。 与从左到右的文本一样,按单元格宽度确定换行位置。
  2. 逐行重排。 解析出的级别决定该行字素的显示顺序。
  3. 阿拉伯文塑形。 字母根据上下文,从阿拉伯文表现形式区块中选取相应字形(独立形、词首形、词中形、词尾形)。一个字母占一个单元格,即使词中出现光标或颜色变化,连写形式也会保留。
  4. 固定顺序。 此时,单元格中的字形已按显示顺序排列。

最后一步是因为 DevTools 会再次对文本运行双向算法,将顺序再次反转。%c 样式无法关闭这一行为,因为 DevTools 的清理器会移除 direction 和 unicode-bidi。因此,驱动会用 LRO 和 PDF(U+202D 和 U+202C)包裹每个包含从右到左字符的 span,强制按从左到右的顺序原样显示单元格中的内容。

半个单元格的偏移

SF Mono 和 Menlo 都没有希伯来文或阿拉伯文字形,因此浏览器会逐字符使用回退字体。在 macOS 上,回退字体是同样为等宽字体的 Courier New;字号为 12 px 时,每个字形宽 7.2012 px,而 Menlo 为 7.2246 px。前者窄 0.3%,足以让整行希伯来文偏移约半个单元格。

CssConsoleDriver 使用设为控制台字体的 canvas 测量每个从右到左的 span,与 DevTools 使用相同的 Blink 字体回退机制,再以 LayoutUnits(1/64 px)为单位按精确差值补齐 span 的宽度。每个 span 最终都与其覆盖的网格单元格等宽。span 最多向内收缩半个单元格,因此宽得多的回退字体会溢出,而不会与下一个 span 重叠。

bun bench/devtools/bidi-shot.ts 在真实的 DevTools 前端中测量结果,使用一个 64×21 的帧,其中包含希伯来文、阿拉伯文、混合文本以及希伯来文中嵌有 emoji 的行:

帧各行右边缘的偏移
未固定顺序就输出的单元格14.5 px
已固定顺序,未适配宽度2.22 px
正式版驱动0.156 px
对照:将每个从右到左字形替换为 x1.67 px

对照组是使用拉丁文本的同一帧。正式版驱动的对齐效果优于对照组,因为从右到左的 span 内的 emoji 也会随 span 一起适配宽度。

从左到右的文本保持不变

不含从右到左字符的文本会跳过所有这些处理。检查在字素分割时完成,每个字素只比较一次,纯从左到右的文本仍沿用原来的绘制路径,因此生成的控制台帧逐字节相同。

从右到左文本支持随 Text.js 1.0 发布。国际化介绍了输入、编辑、终端和 API。