← Blog

Tekst od prawej do lewej w konsoli

Text.js implementuje algorytm dwukierunkowy Unicode i przechodzi wszystkie 770 241 przypadków testowych BidiTest.txt. Tekst hebrajski i arabski renderuje się w kolejności wizualnej, z kształtowaniem pisma arabskiego, wyrównany do siatki komórek.

Adam

Text.js renderuje teraz pismo hebrajskie, arabskie, perskie i inne pisma od prawej do lewej we właściwej kolejności, z połączonymi literami arabskimi, na tej samej siatce komórek co resztę treści.

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

Każdy akapit wybiera kierunek bazowy na podstawie pierwszego znaku o silnej kierunkowości, tak jak dir="auto" w HTML, a akapity pisane od prawej do lewej są wyrównane do prawej. W zagnieżdżonym komponencie <Text> atrybut dir izoluje span tak jak <bdi>, więc nazwa użytkownika o nieznanym kierunku pisma nie może zmienić kolejności otaczającego ją tekstu.

Algorytm

Obsługa pisma od prawej do lewej to implementacja algorytmu dwukierunkowego Unicode (UAX #9, Unicode 18.0), napisana od podstaw w @textjs/core. Wyznacza poziomy osadzenia dla każdego akapitu, uwzględniając jawne osadzenia i izolaty, typy słabe i neutralne oraz pary nawiasów. Następnie kolejność znaków jest zmieniana osobno w każdym zawiniętym wierszu, a znaki lustrzane są zamieniane na poziomach od prawej do lewej, więc (abc) pozostaje (abc) wewnątrz tekstu hebrajskiego.

Unicode publikuje dwa pliki testów zgodności dla tego algorytmu. Oba są zapisane w repozytorium, skompresowane gzipem, a zestaw testów uruchamia każdy przypadek:

  • BidiTest.txt zawiera dane wejściowe jako sekwencje klas bidi, każdą z kierunkami akapitu, do których się odnosi. Text.js przechodzi wszystkie 770 241 przypadków.
  • BidiCharacterTest.txt zawiera rzeczywiste ciągi znaków z oczekiwanymi poziomami i kolejnością. Text.js przechodzi wszystkie 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("");  }}

Od kolejności logicznej do komórek

Wiersz tekstu pisanego od prawej do lewej przechodzi cztery etapy, zanim trafi do konsoli:

  1. Zawijanie w kolejności logicznej. Wiersze są łamane tam, gdzie byłyby łamane w tekście pisanym od lewej do prawej, według szerokości komórek.
  2. Zmiana kolejności w każdym wierszu. Wyznaczone poziomy określają kolejność wyświetlania grafemów wiersza.
  3. Kształtowanie pisma arabskiego. Litery przyjmują formy kontekstowe (izolowaną, początkową, środkową, końcową) z bloków Arabic Presentation Forms. Jedna litera zajmuje jedną komórkę, a formy łączone zachowują się mimo kursora lub zmiany koloru w środku słowa.
  4. Utrwalanie kolejności. Komórki zawierają teraz glify w kolejności wyświetlania.

Ostatni etap jest potrzebny, ponieważ DevTools uruchomiłoby algorytm bidi na tekście po raz drugi i ponownie odwróciło jego kolejność. Styl %c nie może tego wyłączyć, ponieważ mechanizm oczyszczania w DevTools usuwa direction i unicode-bidi. Dlatego sterownik otacza każdy span zawierający znaki pisane od prawej do lewej znakami LRO i PDF (U+202D i U+202C), co wymusza wyświetlanie od lewej do prawej dokładnie tego, co zawierają komórki.

Przesunięcie o pół komórki

Ani SF Mono, ani Menlo nie zawiera glifów hebrajskich czy arabskich, więc przeglądarka dobiera czcionkę zastępczą dla każdego znaku. W systemie macOS jest to Courier New, również o stałej szerokości, z glifem o szerokości 7,2012 px przy rozmiarze 12 px, wobec 7,2246 px w Menlo. Jest o 0,3% węższa, co wystarcza, by cały wiersz tekstu hebrajskiego przesunął się o około pół komórki.

CssConsoleDriver mierzy każdy span pisany od prawej do lewej za pomocą elementu canvas z czcionką konsoli, który korzysta z tego samego mechanizmu dobierania czcionek zastępczych silnika Blink co DevTools, i uzupełnia szerokość spanu o dokładną różnicę w LayoutUnits (1/64 px). Każdy span ma ostatecznie taką szerokość jak zajmowane przez niego komórki siatki. Span jest zwężany najwyżej o pół komórki, więc znacznie szersza czcionka zastępcza wychodzi poza obszar, zamiast nakładać się na następny span.

bun bench/devtools/bidi-shot.ts mierzy wynik w rzeczywistym interfejsie DevTools, na klatce 64×21 z wierszami hebrajskimi, arabskimi, mieszanymi oraz z emoji w tekście hebrajskim:

KlatkaPrzesunięcie prawej krawędzi między wierszami
Komórki wypisane bez utrwalania kolejności14,5 px
Utrwalona kolejność, bez dopasowania szerokości2,22 px
Sterownik w wydanej wersji0,156 px
Próba kontrolna: każdy glif RTL zastąpiony przez x1,67 px

Próba kontrolna to ta sama klatka z tekstem łacińskim. Sterownik w wydanej wersji wyrównuje tekst lepiej niż próba kontrolna, ponieważ emoji wewnątrz spanów pisanych od prawej do lewej są dopasowywane razem z nimi.

Tekst pisany od lewej do prawej bez zmian

Tekst bez znaków pisanych od prawej do lewej pomija wszystkie te etapy. Sprawdzenie odbywa się podczas segmentacji grafemów, jedno porównanie na grafem, a tekst pisany wyłącznie od lewej do prawej jest renderowany tą samą ścieżką co wcześniej, więc jego klatki w konsoli są identyczne bajt po bajcie.

Obsługa tekstu pisanego od prawej do lewej jest dostępna w Text.js 1.0. Internacjonalizacja opisuje wprowadzanie tekstu, edycję, terminale i API.