Текст справа налево в консоли
Text.js реализует двунаправленный алгоритм Unicode и проходит все 770 241 тест из BidiTest.txt. Иврит и арабский отображаются в визуальном порядке с формообразованием арабских букв и выравниванием по сетке ячеек.
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(""); }}От логического порядка к ячейкам
Строка текста с направлением справа налево проходит четыре этапа, прежде чем попасть в консоль:
- Перенос в логическом порядке. Строки переносятся там же, где переносился бы текст слева направо, по ширине ячеек.
- Изменение порядка каждой строки. Определённые уровни задают порядок отображения графем строки.
- Формирование арабских букв. Буквы принимают контекстные формы (изолированную, начальную, срединную, конечную) из блоков Arabic Presentation Forms. Одна буква занимает одну ячейку, а соединённые формы сохраняются при появлении курсора или смене цвета в середине слова.
- Фиксация порядка. Теперь ячейки содержат глифы в порядке отображения.
Последний этап нужен потому, что 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 заменён на x | 1,67 пкс |
Контрольный вариант — тот же кадр с латинским текстом. Драйвер из релиза выравнивает строки лучше контрольного варианта, потому что эмодзи внутри span с направлением справа налево подгоняются вместе с ними.
Текст слева направо без изменений
Текст без символов с направлением справа налево пропускает все эти этапы. Проверка выполняется при сегментации графем, по одному сравнению на графему, а текст только с направлением слева направо отрисовывается тем же путём, что и раньше, поэтому его кадры в консоли побайтово идентичны.
Поддержка текста справа налево входит в Text.js 1.0. Раздел «Интернационализация» описывает ввод, редактирование, терминалы и API.