← 部落格

主控台中的由右至左文字

Text.js 實作 Unicode 雙向演算法,並通過 BidiTest.txt 的全部 770,241 個測試案例。希伯來文與阿拉伯文依視覺順序渲染,支援阿拉伯文字形處理,並對齊儲存格網格。

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. 塑形阿拉伯字母。 字母會從 Arabic Presentation Forms 區塊取得對應上下文的字形(獨立形、字首形、字中形、字尾形)。一個字母佔一個儲存格,即使單字中間出現游標或顏色變化,也能保留連接字形。
  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 前端測量結果,使用包含希伯來文、阿拉伯文、混合文字及希伯來文中插入 emoji 等文字行的 64×21 影格:

影格各列右邊緣的偏移
未固定順序便輸出的儲存格14.5 px
已固定順序,未調整寬度2.22 px
正式版驅動程式0.156 px
對照組:將每個 RTL 字形換成 x1.67 px

對照組是使用拉丁文字的相同影格。正式版驅動程式比對照組對齊得更好,因為從右至左 span 中的 emoji 也會一起調整寬度。

從左至右的文字維持不變

沒有從右至左字元的文字會略過上述所有處理。檢查會在字素分段時執行,每個字素比較一次,而純粹從左至右的文字會沿用原本的繪製路徑,因此主控台影格的每個位元組都與先前相同。

從右至左的文字支援隨 Text.js 1.0 推出。國際化涵蓋輸入、編輯、終端機與 API。