התאמה לשפות
טקסט מימין לשמאל ובכיווניות מעורבת, עיצוב אותיות בערבית ותרגום משטח המיקוד.
Text.js מרנדר עברית, ערבית, פרסית וכתבים אחרים מימין לשמאל בסדר הנכון, ביישור לרשת התאים, ב-DevTools, בטרמינלים ובמראות DOM. הוא מממש את האלגוריתם הדו-כיווני של Unicode (UAX #9, Unicode 18.0) ועובר את חבילות בדיקות התאימות הרשמיות: כל 770,241 המקרים של BidiTest.txt (כל קלט בכל כיוון פסקה) וכל 91,707 המקרים של BidiCharacterTest.txt.
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> );}כיוון
כל פסקה (כל שורה של <Text> המופרדת באמצעות \n) בוחרת את כיוון הבסיס שלה לפי התו החזק הראשון שלה, כפי שעושה dir="auto" ב-HTML. פסקאות מימין לשמאל מיושרות לימין כברירת מחדל.
<Text>שלום</Text> {/* auto: RTL, right-aligned */}<Text dir="rtl">hello!</Text> {/* "!hello", right-aligned */}<Text dir="ltr">שלום!</Text> {/* LTR paragraph: "םולש!" reads correctly left of the "!" */}<Text align="left">שלום</Text> {/* RTL order, pinned to the left edge */}align | פסקה משמאל לימין | פסקה מימין לשמאל |
|---|---|---|
"start" (ברירת מחדל) | שמאל | ימין |
"end" | ימין | שמאל |
"left" / "center" / "right" | כפי שנכתב | כפי שנכתב |
ברכיב <Text> מקונן, dir מבודד את ה-span כמו <bdi> ב-HTML: התוכן שלו מסודר בנפרד ואינו יכול לשנות את סדר הטקסט סביבו. יש להשתמש בו לשמות, לכותרות ולטקסט אחר שהמשתמשים מספקים ושכיוונו אינו ידוע.
<Text>user <Text dir="auto">{name}</Text> replied 3 times</Text>// name = "שלום!" → user !םולש replied 3 times (isolated: "!" ends the name)// without dir → user םולש! replied 3 times ("!" resolves with the Latin text around it)אופן הרינדור
- גלישת שורות בסדר לוגי. השורות נשברות בדיוק כמו בטקסט משמאל לימין, לפי רוחב התאים.
- סידור מחדש של כל שורה. האלגוריתם מחשב רמות הטמעה לכל הפסקה (הטמעות ובידודים מפורשים, טיפוסים חלשים וניטרליים, סוגריים מזווגים), ואז מסדר מחדש כל שורה שנשברה בנפרד (כללים L1–L2). סוגריים ותווים אחרים בעלי תמונת ראי מתחלפים ברמות מימין לשמאל (L4), כך ש-
(abc)נשאר(abc)בתוך עברית. - עיצוב אותיות בערבית. האותיות מקבלות את צורותיהן ההקשריות (מבודדת, תחילית, אמצעית, סופית) מבלוקי Arabic Presentation Forms, המחושבות בסדר לוגי בתוך כל רצף ברמה אחידה בשורה (ZWJ ו-ZWNJ נחשבים במיקומם). אות אחת תופסת תא אחד, צורות מחוברות נשמרות בכל גבול של span (סמן, שינוי צבע), והמסוף לעולם אינו צריך לעצב אותן.
- קיבוע הסדר. התאים מכילים כעת גליפים בסדר התצוגה. דפדפנים וטרמינלים רבים היו מפעילים עליהם את האלגוריתם הדו-כיווני פעם נוספת, ולכן מנהלי ההתקן נועלים את הסדר:
| פלט | אופן קיבוע הסדר |
|---|---|
DevTools (CssConsoleDriver) | מקטעי span שמכילים תווים מימין לשמאל נעטפים ב-LRO … PDF (U+202D / U+202C). מנגנון הניקוי של %c ב-DevTools מסיר את direction ואת unicode-bidi. |
טרמינלים (AnsiDriver, text run) | שורות עם טקסט מימין לשמאל נעטפות ב-LRO … PDF עבור טרמינלים שמפעילים את האלגוריתם הדו-כיווני לכל שורה. renderToTerminal עובר למצב המפורש ECMA-48 BDSM (CSI 8 l, שטרמינלים מבוססי VTE מכבדים) פעם אחת בכניסה וחוזר למצב המרומז (CSI 8 h) ביציאה; פריימים חד-פעמיים של renderToAnsi שמכילים טקסט מימין לשמאל מתחילים ב-CSI 8 l. יש להעביר bidi: "none" כדי לפלוט תאים כפי שהם. |
מראות DOM (<ConsoleMirror>) | המראה מוגדרת כ-direction: ltr (כך שדף מארח בכיוון RTL אינו יכול להפוך אותה), ומקטעי span שמכילים טקסט מימין לשמאל מקבלים unicode-bidi: bidi-override. |
<Box title>, <Box bottomTitle> ו-<Divider title> עוברים את אותם שלבים (drawLogicalText(buffer, x, y, text, …) מצייר כך כל שורה בודדת), ולכן התוויות שלהם מוצגות בסדר הנכון ובצורות מחוברות.
טקסט ללא תווים מימין לשמאל מדלג על כל זה. מודל טקסט שכולו משמאל לימין מזוהה בזמן חלוקתו לגרפמות (השוואה אחת לכל גרפמה) ומצויר במסלול שלא השתנה; הפריימים במסוף זהים ברמת הבתים.
רוחבי תאים
רשימת הגופנים הכלולה היא "SF Mono", Menlo, "Cascadia Mono", Consolas, monospace. אף אחד מהגופנים האלה אינו מכיל גליפים בעברית או בערבית, ולכן הדפדפנים משתמשים בגופן חלופי לכל תו. ב-macOS זהו Courier New, גופן ברוחב אחיד של 0.6 em: 7.2012 px לגליף בגודל 12 px לעומת 7.2246 px של Menlo. הוא צר יותר ב-0.3%, די כדי להסיט שורה שלמה בעברית בכחצי תא.
CssConsoleDriver מודד כל span מימין לשמאל באמצעות canvas בגופן המסוף (אותם עיצוב אותיות של Blink וגופנים חלופיים שבהם DevTools משתמש), ואז מוסיף לו ריווח לפי ההפרש המדויק ב-LayoutUnit (1/64 px). רוחב ה-span משתווה לרוחב תאי הרשת שהוא מכסה (גרפמה רחבה כגון אמוג'י נחשבת לשניים). span מכווץ פנימה לכל היותר בחצי תא, ולכן גופן חלופי ברוחב משתנה (Times New Roman לצד Cascadia Mono ב-Windows) חורג מגבולותיו במקום לאפשר ל-span הבא לחפוף לגליפים שלו. המדידה נערכה בממשק DevTools האמיתי (Chromium 151) על פריים בגודל 64×21 עם שורות בעברית, בערבית, בשילוב שפות ועם אמוג'י בתוך עברית, וכותרות תיבות בכיוון RTL (bun bench/devtools/bidi-shot.ts):
| פריים | סטיית הקצה הימני בין השורות |
|---|---|
| תאים שנרשמו ללא קיבוע | 14.5 px (DevTools מסדר ומעצב מחדש) |
| קיבוע ללא התאמת רוחב | 2.22 px |
| מנהל ההתקן הכלול | 0.156 px |
ביקורת: אותם תאים, כל גליף RTL מוחלף ב-x | 1.67 px |
הפריים הכלול תואם לביקורת הלטינית בכל שורה פרט לשורת האמוג'י, שבה הוא משיג תוצאה טובה יותר: רוחב האמוג'י מותאם עם ה-span מימין לשמאל שסביבם (0.03 px), בעוד שרוחב שורת האמוג'י משמאל לימין בביקורת אינו מותאם (1.66 px).
DevTools מצייר כל span של %c עם contain: paint ומצמיד כל אחד מהם בנפרד לפיקסלים של המכשיר, כך שבמקום שבו שני רקעים נפגשים במיקום שאינו שלם, הפיקסל שביניהם עלול לא להשתייך לאף אחד מהם ולהשאיר קו דק ברוחב 1 px. ב-Chrome וב-Edge, span עם רקע מרחיב אותו ב-1 px מתחת לשכן אטום מימין, ו-span שרוחבו הותאם מוסיף את הפיקסל הזה לריווח הפנימי שלו. באף אחד מארבעת הפריימים לעיל אין קו דק.
יש להעביר measureText: false כדי להשבית את התאמת הרוחב, או לספק פונקציית מדידה משלכם בצורת (text, bold, italic) => px.
קלט ועריכה
<Input> ו-<Textarea> שומרים על סמן לוגי. מקשי החצים נעים בטקסט לפי סדר האחסון, כך שבטקסט מימין לשמאל ← נע חזותית ימינה. סמן הבלוק יושב על התו הלוגי שהוא מכסה, בכל מקום שבו הסידור מחדש ממקם אותו. בתוך רצף מימין לשמאל זהו התא שמשמאל לתו הקודם. גם הבחירה היא טווח לוגי, ולכן ההדגשה שלה עשויה לכסות תאים שאינם רציפים בשורות בעלות כיוונים מעורבים. הגלילה האופקית של <Input> חד-שורי עוקבת אחר העמודה הלוגית, כך שערך ארוך מימין לשמאל עלול לגלול את הסמן אל מחוץ לתצוגה. סמן על רווח או בסוף הטקסט מצויר כרווח קשיח מבודד, כך שהוא נשאר ניטרלי ולעולם אינו מצטרף למספרים באף אחד מצדדיו.
מגבלות
- מפרידי פסקאות. שורות מתפצלות ב-
\n. התו U+2029 ומפרידי פסקאות אחרים מסיימים פסקה דו-כיוונית אך אינם מתחילים שורה חדשה. - טאבים. טאבים מורחבים לרווחים לפני הפעלת האלגוריתם הדו-כיווני, ולכן הם מתנהגים כרווח לבן (WS), ולא כמפרידי מקטעים (S).
- עיצוב אותיות. רק אותיות בעלות צורות תצוגה מעוצבות: ערבית, פרסית, אורדו והאותיות האחרות בטווחים U+FB50–U+FDFF ו-U+FE70–U+FEFF. לאם-אליף נשאר כשתי אותיות מחוברות בשני תאים, ולא כליגטורה של גליף אחד. סורית, N'Ko וכתבים מחוברים אחרים שומרים על צורותיהם הבסיסיות.
- העתקה. טקסט שמועתק מהמסוף הוא בסדר התצוגה (עם סימני
LRO/PDF), ולא בסדר לוגי. - טרמינלים. טרמינלים שמתעלמים גם מ-BDSM וגם מ-
LROומפעילים אלגוריתם דו-כיווני משלהם (למשל כאשר התמיכה הדו-כיוונית מופעלת ב-iTerm2) עשויים לסדר מחדש שורות מימין לשמאל פעם נוספת. - מראות DOM.
<ConsoleMirror>אינו מתאים רוחבים, ולכן רצפים מימין לשמאל בו עשויים לסטות בשיעור של עד 0.3% מרוחבם.
תרגום משטח המיקוד
<FocusPad> מתוך @textjs/react/dom (שגם <TextCanvas> מרנדר) מנחה מבקרים לפתוח את המסוף, ללחוץ על הדף וללחוץ על מקש. הטקסט שלו הוא באנגלית כברירת מחדל. כדי לתרגם אותו, יש להעביר messages עם המחרוזות שיש להחליף. מפתחות שלא נכללו שומרים על הטקסט האנגלי שלהם, המופיע ב-defaultFocusPadMessages.
import { FocusPad, type FocusPadMessages } from "@textjs/react/dom";const ja: Partial<FocusPadMessages> = { openConsole: "コンソールを開く", step: (n, total) => `${total}ステップ中${n}ステップ目`, then: (action) => `次にここをクリックして、${action}。`, shortcutOpens: (shortcut) => <>{shortcut}でコンソールが開きます。</>,};<FocusPad root={root} hints={[{ keys: "↑ ↓", label: "件数" }]} action="↑ ↓を押します" messages={ja} />;מחרוזת שמכילה ערך היא פונקציה שמחזירה את המשפט כולו, כך שהתרגום יכול למקם את הערך בכל מקום בו. step, pressShortcut, then, defaultAction ו-shortcutOpens הן פונקציות. step מקבלת את מספר השלב ואת מספר השלבים. then מקבלת את ה-Prop בשם action, או את הטקסט של defaultAction כשאין action. defaultAction מקבלת את המקשים של הרמז הראשון, או null כשאין רמזים. pressShortcut מקבלת את מקשי קיצור המקשים עבור קוראי מסך, ו-shortcutOpens מקבלת את קיצור המקשים כרכיבי <kbd>.
messages מכסה גם את התוויות שקוראי מסך מכריזים ואת בקרת גודל המסוף (sizeControl). <TextCanvas> מקבל את אותו Prop בשם messages. גם <ViewportControl> מקבל אותו ומשתמש במפתחות של בקרת הגודל. תוויות מקשים כגון ⌘⌥J, Ctrl+Shift+J ו-Enter אינן מתורגמות.
API
import { reorderLine, paragraphDirection, resolveLevels, bidiClass, shapeArabicCodePoints } from "@textjs/core";reorderLine("שלום (abc) 123!"); // "!123 (abc) םולש": one line in display orderreorderLine("مرحبا", "auto", { shape: true }); // presentation forms, display orderparagraphDirection("123 שלום"); // "rtl" (rules P2–P3)resolveLevels(types, codePoints, direction), applyLineRules ו-reorderIndices חושפים את האלגוריתם עצמו: הכללים P1–I2, L1 ו-L2.