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.
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.txtzawiera 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.txtzawiera 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:
- 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.
- Zmiana kolejności w każdym wierszu. Wyznaczone poziomy określają kolejność wyświetlania grafemów wiersza.
- 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.
- 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:
| Klatka | Przesunięcie prawej krawędzi między wierszami |
|---|---|
| Komórki wypisane bez utrwalania kolejności | 14,5 px |
| Utrwalona kolejność, bez dopasowania szerokości | 2,22 px |
| Sterownik w wydanej wersji | 0,156 px |
Próba kontrolna: każdy glif RTL zastąpiony przez x | 1,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.