رفتن به محتوا

termenal-web

  • مخزن: https://github.com/sbay-dev/termenal-web · کامیت خوانده‌شده: 4a9e20c (۲۰۲۶-۰۷-۰۹، کلون سطحی) · مجوز: MIT (LICENSE، © 2026 sbay-dev) · زبان: TypeScript · سکو: مرورگر (دمو) + پل PTY در Node روی WebSocket
  • دسته: شبیه‌ساز ترمینال (مرورگری؛ نمونهٔ اولیه) + کتابخانه (@termenal-web/bidi، /shaping، /terminal)

یک ترمینال مرورگری که هدفش نمایش عربی (و دیگر خط‌های راست‌به‌چپ) با پیوستن واقعی حروف به روش OpenType و با UAX #9 است. این پروژه «خواهر مرورگری» sbay-dev/termenal-Ar است که یک فورک از Windows Terminal است. این پروژهٔ خواهر ارجاع داده شده اما در مخزن گنجانده نشده است: README به آن لینک می‌دهد و packages/bidi/src/index.ts:4,17,61 می‌گوید RTL_RANGES و paragraphReadingDirection «عیناً» (ported verbatim) از termenal-Ar/src/renderer/atlas/DWriteTextAnalysis.cpp منتقل شده‌اند. حدود ۱٫۹ هزار خط کد TS/JS از خود پروژه دارد. README خودش را «Early / in design» معرفی می‌کند. سه بستهٔ کوچک با تست‌های vitest و یک برنامهٔ دموی ۱٬۰۹۷ خطی عرضه می‌کند.

چگونه کار می‌کند (تأییدشده در کد)

Section titled “چگونه کار می‌کند (تأییدشده در کد)”
  • خط لوله (apps/demo/src/terminal.ts:1-12): شل (node-pty، apps/terminal-server/src/server.mjs:71) ← WebSocket ← @xterm/headless (پارسر VT + شبکه؛ نه هستهٔ Rust/WASM ادعاشده) ← رشتهٔ منطقی هر ردیف ← دوجهتی (bidi) + HarfBuzz ← طرح‌های گلیف در Canvas2D (glyphToPath + Path2D، terminal.ts:404-421). هیچ کدی برای WebGPU/WebGL وجود ندارد.
  • دوجهتی (packages/bidi/src/index.ts:96-98): سطوح جاسازی (embedding levels) از bidi-js می‌آیند (یک پیاده‌سازی واقعی از UAX #9). getVisualOrder (:104-119) خروجی getReorderSegments را معکوس می‌کند. اما سطح پاراگراف از یک روش حدسی سفارشی می‌آید (paragraphReadingDirection، :66-89): اکثریت نویسه‌های قوی برنده است، در حالت تساوی نخستین نویسهٔ قوی تعیین‌کننده است، و فقط ASCII A–Z/a–z چپ‌به‌راستِ قوی حساب می‌شوند. این همان P2/P3 در UAX #9 نیست. سپس این مقدار به‌عنوان سطح پایه به bidi-js تحمیل می‌شود (packages/terminal/src/index.ts:136-140).
  • تفکیک تکه‌ها (packages/terminal/src/index.ts:91-104): ردیف‌ها را به بیشینه بازه‌های هم‌سطح تقسیم می‌کند. reorderRunsVisually (:149-189) معکوس‌سازی درستی طبق قاعدهٔ L2 در سطح تکه (run) است.
  • شکل‌دهی (shaping) (packages/shaping/src/index.ts:144-174): harfbuzzjs برای هر تکه، با تنظیم صریح جهت پیش از guessSegmentProperties. خط (script) حدس زده می‌شود. جایگزینی قلم از طریق یک FontResolver انجام می‌شود (terminal.ts:96-100 برای تکه‌های RTL یا متن بلوک عربی، قلم عربی را برمی‌گزیند).
  • نگاشت سلول‌ها (apps/demo/src/terminal.ts:248-283): نویسه‌های هر سلول xterm به هم چسبانده می‌شوند و colOfOffset[] هر آفست UTF-16 را به ستون بافرش برمی‌گرداند. پهنا از wcwidth در xterm.js می‌آید (عربی = ۱ سلول برای هر نقطه‌کد پایه، پس نشانه‌ها در همان سلول ادغام می‌شوند). computeRowLayout (:330-398) جایگشت‌های colToVisual / visualToCol را می‌سازد.
  • دو حالت چیدمان (terminal.ts:311-322). در حالت فیزیکی (صفحهٔ جایگزین، هر پس‌زمینهٔ غیرپیش‌فرض، یا نویسه‌های کادرکشی U+2500–259F در ردیف)، تکه‌ها در ستون‌هایی که برنامه تعیین کرده می‌مانند و فقط سلول‌های درون یک تکهٔ RTL قرینه می‌شوند. در حالت پاراگراف (خروجی سادهٔ شل)، تکه‌ها کاملاً بازچینی می‌شوند و ردیف‌های با پایهٔ RTL به لبهٔ شبکه راست‌چین می‌شوند.
  • جای‌گذاری گلیف‌ها: گلیف‌ها در band.startVisualCol * cellW رسم می‌شوند، اما قلم سپس به اندازهٔ xAdvance در HarfBuzz جلو می‌رود (:408-421)، و به سلول‌ها چفت (snap) نمی‌شود. بنابراین یک تکهٔ عربیِ تناسبی ممکن است n سلولی را که شبکه به آن داده، بیش یا کم پر کند.
  • مکان‌نما (cursor) (:481-490): در colToVisual[cursorX] رسم می‌شود، پس هم‌سو با متن رسم‌شده قرینه می‌شود.
  • انتخاب/کپی (:771-800): تست برخورد (hit-testing) نگاشت پیکسل ← ستون دیداری ← visualToCol ← سلول منطقی را انجام می‌دهد. بازه‌های انتخاب بر حسب سلول‌های منطقی نگه داشته می‌شوند و selectionText() بافر را به ترتیب منطقی می‌خواند، پس متن کپی‌شده منطقی است. هایلایت برای هر سلول منطقی در موقعیت دیداری‌اش رسم می‌شود، که باعث می‌شود انتخابِ متن با جهت‌های مختلط از نظر دیداری ناپیوسته باشد، همان‌طور که باید.
  • ورودی (:636-676): یک نگاشت سادهٔ keydown ← بایت‌ها. هیچ رسیدگی‌ای به compositionstart/end (IME) ندارد، NFC ندارد، و منطق ویژه‌ای برای نیم‌فاصله (ZWNJ) ندارد. نیم‌فاصلهٔ فارسی با Shift+Space فقط وقتی کار می‌کند که مرورگر e.key را یک نویسهٔ تکی گزارش کند.
  • تست‌ها: فایل‌های vitest در packages/{bidi,shaping,terminal}/test/*.test.ts (حدود ۳۲۰ خط) جدول‌های بازه، روش حدسی اکثریت/نخستین قوی، تقسیم تکه‌ها، بازچینی L2، پیوستن حروف (شناسهٔ گلیف‌ها با فرم‌های جدا متفاوت است) و پوشش .notdef را می‌آزمایند. دمو/رندرکننده هیچ تستی ندارد.
  • آزمایش اولیه (spike) (research/arabic-shaping-spike/): یک اسکریپت اثبات در Node به‌همراه evidence.json که قلم‌های سیستمی ویندوز را بارگذاری می‌کند.
لایه وضعیت شواهد
۱ رمزگذاری و یکسان‌سازی ❌ هیچ‌جا رسیدگی به NFC/NFD نیست؛ به رمزگشایی UTF-8 در xterm.js تکیه می‌کند
۲ ورودی (چیدمان‌ها، نیم‌فاصله، IME) ◐ keydown خام ← e.key (terminal.ts:636-676). بدون ترکیب IME، بدون آگاهی از نیم‌فاصله
۳ پهنا ◐ تعداد سلول‌ها از wcwidth در xterm.js می‌آید. پیشروی‌های شکل‌داده‌شده با سلول‌ها جفت نمی‌شوند (:408-421)، پس پهنای رسم‌شده ممکن است با شبکه نخواند
۴ شکل‌دهی ✅ harfbuzzjs برای هر تکهٔ دوجهتی، جهت صریح، پیوستن حروفِ تست‌شده (packages/shaping)
۵ دوجهتی ◐ سطوح واقعی UAX #9 از طریق bidi-js، اما سطح پاراگراف با روش حدسی اکثریت تعیین می‌شود. سمت ترمینال، برای هر ردیف. رسیدگی‌اش به ردیف‌های TUI (حالت فیزیکی) ابتکاری است
۶ مکان‌نما، انتخاب و کپی ✅ جایگشت colToVisual/visualToCol. کپی به ترتیب منطقی است (:771-800)
۷ قلم‌ها ◐ دو قلم که کاربر تأمین می‌کند (تک‌فاصله + عربی)، FontResolver برای هر تکه، hasFullCoverage. بدون زنجیرهٔ جایگزینی سیستمی
۸ مالتی‌پلکسرها ❌ به آن پرداخته نشده
۹ برنامه‌ها و چارچوب‌های TUI ◐ صفحهٔ جایگزین یا ردیفی با نویسه‌های کادر/پس‌زمینه به شبکهٔ فیزیکی سنجاق می‌شود (:311-322). تکه‌ها درون خود قرینه می‌شوند، نه این‌که بازچینی شوند
  • هستهٔ VT مبتنی بر WebAssembly (Rust alacritty_terminal/vte): وجود ندارد. دمو از @xterm/headless استفاده می‌کند (terminal.ts:14). SPEC-0001 D-8 آن را «planned» علامت زده است.
  • اطلس گلیف WebGPU (با پشتیبان WebGL2): وجود ندارد. رندر با Path2D در Canvas2D برای هر گلیف در هر فریم انجام می‌شود (بدون اطلس). D-3 آن را «planned» علامت زده است.
  • «قوی‌تر و سریع‌تر از xterm.js»: هیچ سنجه‌ای (benchmark) ندارد. پرکردن مسیر برای تک‌تک گلیف‌ها احتمالاً کندتر است.
  • «UAX #9»: فقط برای سطوح جاسازی درست است. جهت پاراگراف عمداً از آن فاصله می‌گیرد (قاعدهٔ اکثریت).
  • هم‌سانی با termenal-Ar: در توضیحات کد ادعا شده. کد پروژهٔ خواهر در این مخزن نیست، پس اینجا قابل بررسی نیست.

آنچه می‌آموزیم یا برمی‌داریم

Section titled “آنچه می‌آموزیم یا برمی‌داریم”
  • مدل بافر منطقی ← جایگشت colToVisual/visualToCol برای هر ردیف یک مدل تمیز و آزمودنی برای مکان‌نما، تست برخورد و کپی منطقی است. ارزش دارد آن را به‌عنوان طراحی مرجع خودمان بپذیریم (MIT است، پس با ذکر منبع قابل استفادهٔ مجدد است).
  • تفکیک «فیزیکی در برابر پاراگراف» اذعانی صادقانه به مشکل TUI است: هرگز در عرض ستون‌هایی که برنامه چیده بازچینی نکن. ماشهٔ آن ابتکاری است (رنگ پس‌زمینه / نویسه‌های کادر)، ولی ایده درست است. آن را با یک سازوکار صریحِ انتخابی (opt-in) مثل دنباله‌های گریز حالت BiDi مقایسه کنید (ECMA-48 / پیشنهاد BiDi گروه Terminal-wg).
  • نکتهٔ دردسرساز harfbuzzjs، که در packages/shaping/src/index.ts:79-86 و README آزمایش اولیه مستند شده: جهت باید به‌صورت enum عددی باشد و پیش از guessSegmentProperties تنظیم شود، وگرنه بافر شکل‌داده‌نشده می‌ماند.
  • یک مسیر سریع LTR (hasAnyStrongRtl: اگر هیچ نقطه‌کدی ≥ U+0590 نیست، از دوجهتی بگذر) کم‌هزینه است و ارزش کپی‌کردن دارد.
  • الگوهای تست «شناسهٔ گلیف‌های پیوسته ≠ شناسهٔ گلیف‌های جدا» و «بدون .notdef» را بررسی می‌کنند. این‌ها یک تست دودی (smoke test) خوب و وابسته به قلم برای شکل‌دهی هستند.

محدودیت‌ها و الگوهای نادرست

Section titled “محدودیت‌ها و الگوهای نادرست”
  • جهت پاراگراف بر اساس اکثریت نویسه‌های قوی غیراستاندارد است. با P2/P3 در UAX #9 و با انتظار برنامه‌ها (و FriBidi) در تضاد است. خطی که بیشترش فارسی است و با git شروع می‌شود، به RTL برمی‌گردد.
  • دوجهتیِ سمت ترمینال روی همهٔ خروجی‌های ساده (حالت پاراگراف) متنی را که شاید یک برنامهٔ آگاه از دوجهتی قبلاً مرتب کرده، دوباره بازچینی می‌کند؛ یعنی بازچینی دوگانه. هیچ دنبالهٔ گریزی نیست که اجازه دهد برنامه‌ها مالک دوجهتی باشند.
  • گلیف‌ها با سلول‌ها جفت نمی‌شوند، پس تکه‌های عربی از بازهٔ سلولی‌شان بیرون می‌زنند یا به آن نمی‌رسند. مستطیل‌های پس‌زمینه، مکان‌نما و انتخاب برای هر سلول رسم می‌شوند، پس به‌وضوح با گلیف‌ها ناهم‌تراز خواهند بود.
  • روش حدسی تشخیص TUI شکننده است. یک خط سادهٔ عربی با پس‌زمینهٔ رنگیِ اعلان (prompt) بی‌صدا حالت را عوض می‌کند.
  • IME ندارد، یکسان‌سازی ندارد، و بازه‌های RTL جدول‌های سخت‌کدشده‌اند نه داده‌های Bidi_Class یونیکد (مثلاً U+08A0 به بعد که برای فارسی مهم است پوشش داده شده، ولی رده‌بندی زمخت است).
  • از جنبهٔ مثبت، از جایگزینی با فرم‌های نمایشی (presentation forms) یا ترفندهای بافر با ترتیب دیداری (visual order) استفاده نمی‌کند. بافر منطقی می‌ماند.
  • بلوغ: یک کامیت سطحی، در حد دمو، ویندوزمحور (مسیرهای قلم در آزمایش اولیه C:\Windows\Fonts، پیش‌فرض ConPTY).

یک پروژهٔ مهدی بانشی · mbaneshi.ir