Rechts-nach-links-Text in der Konsole
Text.js implementiert den bidirektionalen Unicode-Algorithmus und besteht alle 770.241 Fälle aus BidiTest.txt. Hebräisch und Arabisch werden in visueller Reihenfolge mit arabischer Zeichenformung am Zellenraster ausgerichtet gerendert.
Text.js rendert jetzt Hebräisch, Arabisch, Persisch und andere von rechts nach links geschriebene Schriften in der richtigen Reihenfolge, mit verbundenen arabischen Buchstaben, auf demselben Zellenraster wie alles andere.
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> );}Jeder Absatz bestimmt seine Grundrichtung anhand seines ersten starken Zeichens, wie dir="auto" in HTML, und Absätze mit Rechts-nach-links-Richtung werden rechtsbündig ausgerichtet. Bei einem verschachtelten <Text> isoliert dir den Span wie <bdi>, sodass ein Benutzername mit unbekannter Schreibrichtung den umgebenden Text nicht umordnen kann.
Der Algorithmus
Die Unterstützung für Rechts-nach-links-Text implementiert den Unicode Bidirectional Algorithm (UAX #9, Unicode 18.0), von Grund auf neu geschrieben in @textjs/core. Er ermittelt die Einbettungsebenen für jeden Absatz, einschließlich expliziter Einbettungen und Isolate, schwacher und neutraler Typen sowie Klammerpaare. Anschließend wird jede umgebrochene Zeile einzeln umgeordnet, und gespiegelte Zeichen werden auf Rechts-nach-links-Ebenen ausgetauscht, sodass (abc) innerhalb von Hebräisch (abc) bleibt.
Unicode veröffentlicht zwei Konformitätsdateien für den Algorithmus. Beide liegen gzip-komprimiert im Repository, und die Testsuite führt jeden Fall aus:
BidiTest.txtenthält Eingaben als Folgen von Bidi-Klassen, jeweils mit den Absatzrichtungen, für die sie gelten. Text.js besteht alle 770.241 Fälle.BidiCharacterTest.txtenthält echte Zeichenfolgen mit den erwarteten Ebenen und der erwarteten Reihenfolge. Text.js besteht alle 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(""); }}Von der logischen Reihenfolge zu Zellen
Eine Zeile mit Rechts-nach-links-Text durchläuft vier Schritte, bevor sie die Konsole erreicht:
- In logischer Reihenfolge umbrechen. Zeilen werden wie bei Links-nach-rechts-Text anhand der Zellenbreite umgebrochen.
- Jede Zeile umordnen. Die ermittelten Ebenen bestimmen die Anzeigereihenfolge der Grapheme in der Zeile.
- Arabische Buchstaben formen. Buchstaben erhalten ihre kontextabhängigen Formen (isoliert, initial, medial, final) aus den Blöcken Arabic Presentation Forms. Ein Buchstabe belegt eine Zelle, und verbundene Formen bleiben auch bei einem Cursor oder Farbwechsel mitten im Wort erhalten.
- Die Reihenfolge fixieren. Die Zellen enthalten die Glyphen jetzt in Anzeigereihenfolge.
Der letzte Schritt ist nötig, weil DevTools den Bidi-Algorithmus ein zweites Mal auf den Text anwenden und ihn erneut umkehren würde. Ein %c-Stil kann das nicht abschalten, weil der Sanitizer von DevTools direction und unicode-bidi entfernt. Deshalb umschließt der Treiber jeden Span mit Rechts-nach-links-Zeichen mit LRO und PDF (U+202D und U+202C), was die Anzeige genau des Zelleninhalts von links nach rechts erzwingt.
Eine halbe Zelle Versatz
Weder SF Mono noch Menlo enthält hebräische oder arabische Glyphen, deshalb verwendet der Browser für jedes Zeichen eine Ersatzschrift. Unter macOS ist das Courier New, ebenfalls eine nichtproportionale Schrift, mit 7,2012 px pro Glyphe bei 12 px gegenüber 7,2246 px bei Menlo. Sie ist 0,3 % schmaler, genug, um eine volle Zeile Hebräisch um etwa eine halbe Zelle zu verschieben.
CssConsoleDriver misst jeden Rechts-nach-links-Span mit einem Canvas in der Konsolenschrift, der denselben Blink-Schrift-Fallback wie DevTools verwendet, und ergänzt den Span um die exakte Differenz in LayoutUnits (1/64 px). Jeder Span ist am Ende genauso breit wie die Rasterzellen, die er abdeckt. Ein Span wird um höchstens eine halbe Zelle zusammengezogen, sodass eine deutlich breitere Ersatzschrift überläuft, statt den nächsten Span zu überlappen.
bun bench/devtools/bidi-shot.ts misst das Ergebnis im echten DevTools-Frontend auf einem 64×21-Frame mit hebräischen, arabischen, gemischten und hebräischen Zeilen mit Emojis:
| Frame | Versatz am rechten Rand über die Zeilen hinweg |
|---|---|
| Zellen ohne fixierte Reihenfolge ausgegeben | 14,5 px |
| Reihenfolge fixiert, ohne Breitenanpassung | 2,22 px |
| Ausgelieferter Treiber | 0,156 px |
Kontrollmessung: jede RTL-Glyphe durch x ersetzt | 1,67 px |
Die Kontrollmessung verwendet denselben Frame mit lateinischem Text. Der ausgelieferte Treiber richtet die Zeilen besser aus als die Kontrollmessung, weil Emojis innerhalb von Rechts-nach-links-Spans zusammen mit diesen angepasst werden.
Links-nach-rechts-Text unverändert
Text ohne Rechts-nach-links-Zeichen überspringt all diese Schritte. Die Prüfung erfolgt während der Segmentierung der Grapheme, mit einem Vergleich pro Graphem, und reiner Links-nach-rechts-Text wird über denselben Pfad wie bisher gezeichnet, sodass seine Konsolen-Frames byteidentisch sind.
Rechts-nach-links-Text ist in Text.js 1.0 enthalten. Internationalisierung behandelt Eingabe, Bearbeitung, Terminals und die API.