← ブログ

コンソールで右から左に書くテキスト

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> と同様にスパンを分離するため、方向が不明なユーザー名が周囲のテキストの順序を変えることはありません。

アルゴリズム

右から左への表示は、@textjs/core でゼロから記述したUnicode双方向アルゴリズム(UAX #9、Unicode 18.0)の実装によって実現しています。明示的な埋め込みと分離、弱い型と中立型、対応する括弧を含め、段落ごとに埋め込みレベルを解決します。その後、折り返された各行を個別に並べ替え、右から左へのレベルでは鏡像文字を入れ替えるため、ヘブライ語の中でも (abc) は (abc) のままです。

Unicodeは、このアルゴリズムの適合性検証用ファイルを2つ公開しています。どちらも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("");  }}

論理順序からセルへ

右から左へのテキストの行は、コンソールに届くまでに4つのステップを通ります。

  1. 論理順序で折り返します。 左から右へのテキストと同じ位置で、セル幅に基づいて改行します。
  2. 各行を並べ替えます。 解決されたレベルによって、行内の書記素の表示順序が決まります。
  3. アラビア文字を字形処理します。 文字はArabic Presentation Formsブロックから、文脈に応じた字形(独立形、語頭形、語中形、語尾形)を取ります。1文字は1セルで、単語の途中にカーソルや色の変更があっても連結形は維持されます。
  4. 順序を固定します。 これで、セルには表示順序でグリフが格納されます。

最後のステップがあるのは、DevToolsがテキストに双方向処理を再度適用し、もう一度反転させてしまうためです。DevToolsのサニタイザーが direction と unicode-bidi を除去するため、%c スタイルではこの処理を無効にできません。そこでドライバーは、右から左への文字を含むすべてのスパンを LRO と PDF(U+202DとU+202C)で囲み、セルに格納された内容をそのまま左から右へ表示させます。

半セルのずれ

SF MonoにもMenloにもヘブライ文字やアラビア文字のグリフがないため、ブラウザーは文字ごとにフォールバックします。macOSでは、同じく等幅のCourier Newが使われ、12pxでのグリフ幅はMenloの7.2246pxに対して7.2012pxです。幅が0.3%狭いため、ヘブライ語の行全体では約半セルのずれが生じます。

CssConsoleDriver は、DevToolsと同じBlinkのフォントフォールバックを使うコンソールのフォントで、canvasにより右から左への各スパンを計測し、LayoutUnits(1/64px)単位で差分どおりの余白を加えます。すべてのスパンの幅は、それが占めるグリッドセルの幅と一致します。スパンを縮める量は最大で半セルなので、大幅に幅広いフォールバックフォントは、次のスパンと重なる代わりにはみ出します。

bun bench/devtools/bidi-shot.ts は、ヘブライ語、アラビア語、混在テキスト、ヘブライ語内に絵文字を含む行からなる64×21のフレームを使い、実際のDevToolsフロントエンドで結果を計測します。

フレーム行間の右端のずれ
順序を固定せずにログ出力したセル14.5px
順序を固定、幅調整なし2.22px
製品版ドライバー0.156px
対照:すべてのRTLグリフを x に置換1.67px

対照は、同じフレームをラテン文字のテキストにしたものです。製品版ドライバーでは、右から左へのスパン内の絵文字も一緒に幅調整されるため、対照よりもよく揃います。

左から右へのテキストは変更なし

右から左への文字を含まないテキストは、この処理をすべてスキップします。判定は書記素の分割中に、書記素ごとに1回の比較で行われ、左から右への文字だけのテキストは従来と同じ経路で描画されるため、コンソールのフレームはバイト単位で同一です。

右から左へのテキストはText.js 1.0に搭載されています。国際化では、入力、編集、ターミナル、APIを解説しています。