← Блог

Текст справа налево в консоли

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>  );}

Каждый абзац выбирает базовое направление по первому символу с сильной направленностью, как это делает dir="auto" в HTML, а абзацы с направлением справа налево выравниваются по правому краю. Во вложенном <Text> атрибут dir изолирует span так же, как <bdi>, поэтому имя пользователя с неизвестным направлением письма не может изменить порядок текста вокруг него.

Алгоритм

Поддержка письма справа налево реализует двунаправленный алгоритм Unicode (UAX #9, Unicode 18.0), написанный с нуля в @textjs/core. Он определяет уровни вложенности для каждого абзаца, учитывая явные вложения и изоляты, слабые и нейтральные типы и парные скобки. Затем порядок каждой перенесённой строки меняется отдельно, а зеркальные символы заменяются на уровнях с направлением справа налево, поэтому (abc) остаётся (abc) внутри текста на иврите.

Unicode публикует два файла для проверки соответствия алгоритму. Оба хранятся в репозитории в сжатом формате gzip, и набор тестов выполняет каждый случай:

  • BidiTest.txt содержит входные данные в виде последовательностей классов bidi с указанием направлений абзаца, к которым применим каждый случай. 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 применил бы bidi к тексту повторно и снова развернул его. Стиль %c не может отключить это, поскольку фильтр DevTools удаляет direction и unicode-bidi. Поэтому драйвер обрамляет каждый span с символами, направленными справа налево, символами LRO и PDF (U+202D и U+202C), принудительно отображая слева направо именно то, что содержится в ячейках.

Смещение на пол-ячейки

Ни SF Mono, ни Menlo не содержат глифов иврита или арабского, поэтому браузер подбирает запасной шрифт для каждого символа. В macOS это Courier New, тоже моноширинный, с шириной глифа 7,2012 пкс при размере 12 пкс против 7,2246 пкс у Menlo. Он на 0,3 % уже, чего достаточно, чтобы полная строка на иврите сместилась примерно на пол-ячейки.

CssConsoleDriver измеряет каждый span с направлением справа налево через canvas со шрифтом консоли, используя тот же механизм подбора запасного шрифта Blink, что и DevTools, и добавляет к span отступ на точную разницу в LayoutUnits (1/64 пкс). В результате ширина каждого span равна ширине занимаемых им ячеек сетки. Span сжимается не более чем на пол-ячейки, поэтому значительно более широкий запасной шрифт выходит за границы, а не перекрывает следующий span.

bun bench/devtools/bidi-shot.ts измеряет результат в реальном интерфейсе DevTools на кадре 64×21 со строками на иврите, арабском, со смешанным текстом и эмодзи внутри иврита:

КадрСмещение правого края между строками
Ячейки выведены без фиксации порядка14,5 пкс
Порядок зафиксирован, без подгонки ширины2,22 пкс
Драйвер из релиза0,156 пкс
Контроль: каждый глиф RTL заменён на x1,67 пкс

Контрольный вариант — тот же кадр с латинским текстом. Драйвер из релиза выравнивает строки лучше контрольного варианта, потому что эмодзи внутри span с направлением справа налево подгоняются вместе с ними.

Текст слева направо без изменений

Текст без символов с направлением справа налево пропускает все эти этапы. Проверка выполняется при сегментации графем, по одному сравнению на графему, а текст только с направлением слева направо отрисовывается тем же путём, что и раньше, поэтому его кадры в консоли побайтово идентичны.

Поддержка текста справа налево входит в Text.js 1.0. Раздел «Интернационализация» описывает ввод, редактирование, терминалы и API.