{"_id":"bidi-shaper","_rev":"3-14e7dc8e24c1d67ca91b9fd23abeea85","name":"bidi-shaper","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"bidi-shaper","version":"0.1.0","keywords":["bidi","bidirectional","uax9","arabic","shaping","rtl","persian","urdu","unicode","presentation-forms","jspdf","pdfmake","pdfkit","canvas","three.js","reorder"],"author":{"name":"cc1a2b","email":"renhusa9@gmail.com"},"license":"MIT","_id":"bidi-shaper@0.1.0","maintainers":[{"name":"cc1a2b","email":"cc1a2bb@gmail.com"}],"homepage":"https://github.com/cc1a2b/bidi-shaper#readme","bugs":{"url":"https://github.com/cc1a2b/bidi-shaper/issues"},"dist":{"shasum":"7d742924d74decb77d76214e2dd0d762248ebc25","tarball":"https://registry.npmjs.org/bidi-shaper/-/bidi-shaper-0.1.0.tgz","fileCount":30,"integrity":"sha512-4ws6xHpGTmHFaJrkjjFGeu0Emc1rvNx/Nu1QVYJyiR/JcNX2qLoh0PYinV1lFFWmvKzBwn8BVsBKl+crMOnvWA==","signatures":[{"sig":"MEQCIAvVj+kN2NgTxt5smTp8Bs/T4TvEXpLtq/eblJWHmV6OAiB3XA1JW3HR7utudpk4SFnwzLF19x7S/PnZXukCmPCLVg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":709629},"main":"./dist/index.cjs","type":"module","_from":"file:bidi-shaper-0.1.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./jspdf":{"import":{"types":"./dist/adapters/jspdf.d.ts","default":"./dist/adapters/jspdf.js"},"require":{"types":"./dist/adapters/jspdf.d.cts","default":"./dist/adapters/jspdf.cjs"}},"./three":{"import":{"types":"./dist/adapters/three.d.ts","default":"./dist/adapters/three.js"},"require":{"types":"./dist/adapters/three.d.cts","default":"./dist/adapters/three.cjs"}},"./canvas":{"import":{"types":"./dist/adapters/canvas.d.ts","default":"./dist/adapters/canvas.js"},"require":{"types":"./dist/adapters/canvas.d.cts","default":"./dist/adapters/canvas.cjs"}},"./pdfkit":{"import":{"types":"./dist/adapters/pdfkit.d.ts","default":"./dist/adapters/pdfkit.js"},"require":{"types":"./dist/adapters/pdfkit.d.cts","default":"./dist/adapters/pdfkit.cjs"}},"./pdfmake":{"import":{"types":"./dist/adapters/pdfmake.d.ts","default":"./dist/adapters/pdfmake.js"},"require":{"types":"./dist/adapters/pdfmake.d.cts","default":"./dist/adapters/pdfmake.cjs"}},"./package.json":"./package.json"},"scripts":{"dev":"tsup --watch","lint":"eslint .","test":"vitest run","bench":"node bench/bench.mjs","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","generate-data":"tsx scripts/generate-data.ts","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build","test:conformance":"vitest run test/conformance"},"_npmUser":{"name":"cc1a2b","email":"cc1a2bb@gmail.com"},"_resolved":"/mnt/e/Programming/bidi-shaper/bidi-shaper-0.1.0.tgz","overrides":{"esbuild":"^0.28.1"},"_integrity":"sha512-4ws6xHpGTmHFaJrkjjFGeu0Emc1rvNx/Nu1QVYJyiR/JcNX2qLoh0PYinV1lFFWmvKzBwn8BVsBKl+crMOnvWA==","repository":{"url":"git+https://github.com/cc1a2b/bidi-shaper.git","type":"git"},"_npmVersion":"9.2.0","description":"Logical→visual Unicode BiDi (UAX #9) reordering + Arabic contextual shaping for renderers outside the browser (PDF, canvas, WebGL, SVG, terminals, games). Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"20.19.5","typesVersions":{"*":{"jspdf":["./dist/adapters/jspdf.d.ts"],"three":["./dist/adapters/three.d.ts"],"canvas":["./dist/adapters/canvas.d.ts"],"pdfkit":["./dist/adapters/pdfkit.d.ts"],"pdfmake":["./dist/adapters/pdfmake.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.2","tsup":"^8.3.5","eslint":"^9.17.0","vitest":"^4.1.8","bidi-js":"^1.0.3","tinybench":"^6.0.2","@eslint/js":"^9.17.0","typescript":"^5.7.2","@types/node":"^22.10.2","typescript-eslint":"^8.18.1","@vitest/coverage-v8":"^4.1.8"},"_npmOperationalInternal":{"tmp":"tmp/bidi-shaper_0.1.0_1783105015192_0.48421485576156087","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"bidi-shaper","version":"0.1.1","keywords":["bidi","bidirectional","uax9","arabic","shaping","rtl","persian","urdu","unicode","presentation-forms","jspdf","pdfmake","pdfkit","canvas","three.js","reorder"],"author":{"name":"cc1a2b","email":"renhusa9@gmail.com"},"license":"MIT","_id":"bidi-shaper@0.1.1","maintainers":[{"name":"cc1a2b","email":"cc1a2bb@gmail.com"}],"homepage":"https://github.com/cc1a2b/bidi-shaper#readme","bugs":{"url":"https://github.com/cc1a2b/bidi-shaper/issues"},"dist":{"shasum":"0db149d0a0cdc3e73285d2ea54cb1ee7adb66031","tarball":"https://registry.npmjs.org/bidi-shaper/-/bidi-shaper-0.1.1.tgz","fileCount":30,"integrity":"sha512-Av7QWvmVgnhn0hY0NMwuW4d9Okqqjlh8VnYf7CTyxNt7pXy3dpi5huzcbQwo0AredPzZazFGllpNfJvq6G20WA==","signatures":[{"sig":"MEUCIQD9yzLRUURR61Mx+OphWbkROaEfOHESqyGYx62j76iHEgIgLRvOfpDbXEcRnv7ssfGEeCxC0y4Lh8hZneFmv31FCi4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":716422},"main":"./dist/index.cjs","type":"module","_from":"file:bidi-shaper-0.1.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./jspdf":{"import":{"types":"./dist/adapters/jspdf.d.ts","default":"./dist/adapters/jspdf.js"},"require":{"types":"./dist/adapters/jspdf.d.cts","default":"./dist/adapters/jspdf.cjs"}},"./three":{"import":{"types":"./dist/adapters/three.d.ts","default":"./dist/adapters/three.js"},"require":{"types":"./dist/adapters/three.d.cts","default":"./dist/adapters/three.cjs"}},"./canvas":{"import":{"types":"./dist/adapters/canvas.d.ts","default":"./dist/adapters/canvas.js"},"require":{"types":"./dist/adapters/canvas.d.cts","default":"./dist/adapters/canvas.cjs"}},"./pdfkit":{"import":{"types":"./dist/adapters/pdfkit.d.ts","default":"./dist/adapters/pdfkit.js"},"require":{"types":"./dist/adapters/pdfkit.d.cts","default":"./dist/adapters/pdfkit.cjs"}},"./pdfmake":{"import":{"types":"./dist/adapters/pdfmake.d.ts","default":"./dist/adapters/pdfmake.js"},"require":{"types":"./dist/adapters/pdfmake.d.cts","default":"./dist/adapters/pdfmake.cjs"}},"./package.json":"./package.json"},"scripts":{"dev":"tsup --watch","lint":"eslint .","test":"vitest run","bench":"node bench/bench.mjs","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","generate-data":"tsx scripts/generate-data.ts","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build","test:conformance":"vitest run test/conformance"},"_npmUser":{"name":"cc1a2b","email":"cc1a2bb@gmail.com"},"_resolved":"/mnt/e/Programming/bidi-shaper/bidi-shaper-0.1.1.tgz","overrides":{"esbuild":"^0.28.1"},"_integrity":"sha512-Av7QWvmVgnhn0hY0NMwuW4d9Okqqjlh8VnYf7CTyxNt7pXy3dpi5huzcbQwo0AredPzZazFGllpNfJvq6G20WA==","repository":{"url":"git+https://github.com/cc1a2b/bidi-shaper.git","type":"git"},"_npmVersion":"9.2.0","description":"Logical→visual Unicode BiDi (UAX #9) reordering + Arabic contextual shaping for renderers outside the browser (PDF, canvas, WebGL, SVG, terminals, games). Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"20.19.5","typesVersions":{"*":{"jspdf":["./dist/adapters/jspdf.d.ts"],"three":["./dist/adapters/three.d.ts"],"canvas":["./dist/adapters/canvas.d.ts"],"pdfkit":["./dist/adapters/pdfkit.d.ts"],"pdfmake":["./dist/adapters/pdfmake.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.2","tsup":"^8.3.5","eslint":"^9.17.0","vitest":"^4.1.8","bidi-js":"^1.0.3","tinybench":"^6.0.2","@eslint/js":"^9.17.0","typescript":"^5.7.2","@types/node":"^22.10.2","typescript-eslint":"^8.18.1","@vitest/coverage-v8":"^4.1.8"},"_npmOperationalInternal":{"tmp":"tmp/bidi-shaper_0.1.1_1783105917371_0.8358431929810801","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"bidi-shaper","version":"0.1.2","description":"Logical→visual Unicode BiDi (UAX #9) reordering + Arabic contextual shaping for renderers outside the browser (PDF, canvas, WebGL, SVG, terminals, games). Zero dependencies.","keywords":["bidi","bidirectional","uax9","arabic","shaping","rtl","persian","urdu","unicode","presentation-forms","jspdf","pdfmake","pdfkit","canvas","three.js","reorder"],"license":"MIT","author":{"name":"cc1a2b","email":"renhusa9@gmail.com"},"repository":{"type":"git","url":"git+https://github.com/cc1a2b/bidi-shaper.git"},"homepage":"https://github.com/cc1a2b/bidi-shaper#readme","bugs":{"url":"https://github.com/cc1a2b/bidi-shaper/issues"},"type":"module","sideEffects":false,"engines":{"node":">=18"},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","typesVersions":{"*":{"jspdf":["./dist/adapters/jspdf.d.ts"],"pdfmake":["./dist/adapters/pdfmake.d.ts"],"pdfkit":["./dist/adapters/pdfkit.d.ts"],"canvas":["./dist/adapters/canvas.d.ts"],"three":["./dist/adapters/three.d.ts"]}},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./jspdf":{"import":{"types":"./dist/adapters/jspdf.d.ts","default":"./dist/adapters/jspdf.js"},"require":{"types":"./dist/adapters/jspdf.d.cts","default":"./dist/adapters/jspdf.cjs"}},"./pdfmake":{"import":{"types":"./dist/adapters/pdfmake.d.ts","default":"./dist/adapters/pdfmake.js"},"require":{"types":"./dist/adapters/pdfmake.d.cts","default":"./dist/adapters/pdfmake.cjs"}},"./pdfkit":{"import":{"types":"./dist/adapters/pdfkit.d.ts","default":"./dist/adapters/pdfkit.js"},"require":{"types":"./dist/adapters/pdfkit.d.cts","default":"./dist/adapters/pdfkit.cjs"}},"./canvas":{"import":{"types":"./dist/adapters/canvas.d.ts","default":"./dist/adapters/canvas.js"},"require":{"types":"./dist/adapters/canvas.d.cts","default":"./dist/adapters/canvas.cjs"}},"./three":{"import":{"types":"./dist/adapters/three.d.ts","default":"./dist/adapters/three.js"},"require":{"types":"./dist/adapters/three.d.cts","default":"./dist/adapters/three.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:conformance":"vitest run test/conformance","test:coverage":"vitest run --coverage","generate-data":"tsx scripts/generate-data.ts","bench":"node bench/bench.mjs","typecheck":"tsc --noEmit","lint":"eslint .","prepublishOnly":"npm run build"},"overrides":{"esbuild":"^0.28.1"},"devDependencies":{"@eslint/js":"^9.17.0","@types/node":"^22.10.2","@vitest/coverage-v8":"^4.1.8","bidi-js":"^1.0.3","eslint":"^9.17.0","tinybench":"^6.0.2","tsup":"^8.3.5","tsx":"^4.19.2","typescript":"^5.7.2","typescript-eslint":"^8.18.1","vitest":"^4.1.8"},"_id":"bidi-shaper@0.1.2","_integrity":"sha512-JBRKDJ5lIu0DEUJdNVLmXCVLe01k3Y5AO3KoW6vB1MPj9yc0u570uJY4LrM1JusP10Zbr70ClIdVQ2JD+g6R1g==","_resolved":"/mnt/e/Programming/bidi-shaper/bidi-shaper-0.1.2.tgz","_from":"file:bidi-shaper-0.1.2.tgz","_nodeVersion":"20.19.5","_npmVersion":"9.2.0","dist":{"integrity":"sha512-JBRKDJ5lIu0DEUJdNVLmXCVLe01k3Y5AO3KoW6vB1MPj9yc0u570uJY4LrM1JusP10Zbr70ClIdVQ2JD+g6R1g==","shasum":"733ebaec532aabb727a165d4555beaa368d5700e","tarball":"https://registry.npmjs.org/bidi-shaper/-/bidi-shaper-0.1.2.tgz","fileCount":30,"unpackedSize":716481,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGbe62iWxKoURTlKmfK/cUwEWANojoD+MGOpIZiBRQXmAiEAx9ojA//Z3Xu2rEy50+6cEDRmjVUiGQUfeoJtJiRwskU="}]},"_npmUser":{"name":"cc1a2b","email":"cc1a2bb@gmail.com"},"directories":{},"maintainers":[{"name":"cc1a2b","email":"cc1a2bb@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/bidi-shaper_0.1.2_1783108257105_0.8761788073880425"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-03T18:56:54.992Z","modified":"2026-07-03T19:50:57.385Z","0.1.0":"2026-07-03T18:56:55.355Z","0.1.1":"2026-07-03T19:11:57.530Z","0.1.2":"2026-07-03T19:50:57.271Z"},"bugs":{"url":"https://github.com/cc1a2b/bidi-shaper/issues"},"author":{"name":"cc1a2b","email":"renhusa9@gmail.com"},"license":"MIT","homepage":"https://github.com/cc1a2b/bidi-shaper#readme","keywords":["bidi","bidirectional","uax9","arabic","shaping","rtl","persian","urdu","unicode","presentation-forms","jspdf","pdfmake","pdfkit","canvas","three.js","reorder"],"repository":{"type":"git","url":"git+https://github.com/cc1a2b/bidi-shaper.git"},"description":"Logical→visual Unicode BiDi (UAX #9) reordering + Arabic contextual shaping for renderers outside the browser (PDF, canvas, WebGL, SVG, terminals, games). Zero dependencies.","maintainers":[{"name":"cc1a2b","email":"cc1a2bb@gmail.com"}],"readme":"<div align=\"center\">\n\n# bidi-shaper\n\n### Unicode BiDi + Arabic shaping for renderers that can't do either\n\nLogical→visual **UAX #9** reordering · Arabic contextual shaping · lam-alef ligatures · bracket mirroring —<br/>\none plain string out, ready for **jsPDF, pdfmake, PDFKit, canvas, three.js, terminals and game engines**. Zero dependencies.\n\n<p lang=\"ar\" dir=\"rtl\"><em>يُخزَّن النصُّ بترتيب القراءة ويُرسَم بترتيب الرؤية — وهذه المسافة بينهما هي عملي.</em></p>\n\n[![CI](https://github.com/cc1a2b/bidi-shaper/actions/workflows/ci.yml/badge.svg)](https://github.com/cc1a2b/bidi-shaper/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/bidi-shaper?style=flat-square&color=brightgreen)](https://www.npmjs.com/package/bidi-shaper) [![gzipped size](https://img.shields.io/badge/gzipped-15%20kB-blue?style=flat-square)](https://www.npmjs.com/package/bidi-shaper) [![UAX #9 conformance](https://img.shields.io/badge/UAX%20%239-861%2C948%20cases%20pass-3fb950?style=flat-square)](#conformance--all-861948-official-cases) [![Unicode 17.0.0](https://img.shields.io/badge/Unicode-17.0.0-5d3fd3?style=flat-square)](https://www.unicode.org/versions/Unicode17.0.0/) [![zero dependencies](https://img.shields.io/badge/deps-0-brightgreen?style=flat-square)](./package.json) [![types included](https://img.shields.io/npm/types/bidi-shaper?style=flat-square)](https://www.npmjs.com/package/bidi-shaper)\n\n**[📦 npm](https://www.npmjs.com/package/bidi-shaper) · [🕹 Live demo](https://bidi-shaper.vercel.app) · [⭐ GitHub](https://github.com/cc1a2b/bidi-shaper)**\n\n</div>\n\n> **bidi-shaper** runs the two invisible passes every browser and native text stack performs before a glyph hits the screen — the **Unicode Bidirectional Algorithm (UAX #9)** and **Arabic contextual shaping** — and hands you a plain string in final visual order. Renderers that place glyphs one after another (PDF generators, bitmap-font game engines, WebGL text, plotters, e-ink dashboards) draw it correctly, glyph by glyph, left to right. Pure TypeScript, verified against **all 861,948 official Unicode conformance cases**, in **~15 kB** gzipped.\n\n```sh\nnpm install bidi-shaper\n```\n\n```ts\nimport { render } from \"bidi-shaper\";\n\ndoc.text(render(\"مرحبا بالعالم\"), 40, 60); // jsPDF — and now the Arabic is readable\n```\n\n---\n\n## What breaks without it\n\nGive a glyph-by-glyph renderer logical-order Arabic and every one of these goes wrong at once:\n\n| Failure | Naive renderer (raw string, left→right) | Through `render()` |\n|---|---|---|\n| Cursive joining | ✗ isolated, disconnected letters — ك ت ا ب | ✓ contextual forms — <span dir=\"rtl\">كتاب</span> |\n| Reading direction | ✗ RTL words come out reversed, end-first | ✓ RTL runs flow right-to-left, LTR stays LTR |\n| Numbers inside RTL | ✗ drift to the wrong end of the sentence | ✓ stay left-to-right, in place — <span dir=\"rtl\">سنة ١٤٤٧</span> |\n| Mixed Arabic + English | ✗ word order scrambles mid-sentence | ✓ each run in its own direction (UAX #9 levels) |\n| Brackets & parentheses | ✗ `(` points the wrong way on RTL runs | ✓ mirrored per rule L4 — <span dir=\"rtl\">قائمة (أ)</span> |\n| lam + alef | ✗ two stray letters — ل ا | ✓ one mandatory ligature — ﻻ |\n| Harakat / tashkeel | ✗ break joining, or crash the renderer | ✓ transparent to joining; keep or strip them |\n| Persian & Urdu letters | ✗ پ گ چ ژ ٹ ڈ ے left unjoined | ✓ full Arabic-script shaping, not just Arabic |\n\n**Who needs this:** jsPDF · pdfmake · PDFKit-style generators · custom canvas rasterizers · bitmap-font engines · three.js `TextGeometry` / troika-text · SDF/MSDF text · terminal UIs · plotters · e-ink dashboards.\n**Who doesn't:** browser DOM/CSS, native text views, HarfBuzz-based stacks — they already run both passes (and with full OpenType typography).\n\n---\n\n## Install\n\n```sh\nnpm install bidi-shaper\n# or\nyarn add bidi-shaper\n# or\npnpm add bidi-shaper\n```\n\n**Requirements:** Node.js ≥ 18 or any modern browser/bundler · TypeScript optional · zero runtime dependencies. No native modules, no WASM, no fonts shipped.\n\n### Browser / CDN — no build step\n\nEvery release is mirrored on [jsDelivr](https://www.jsdelivr.com/package/npm/bidi-shaper) and [unpkg](https://unpkg.com/browse/bidi-shaper/) automatically:\n\n```html\n<script type=\"module\">\n  import { render, analyze } from \"https://cdn.jsdelivr.net/npm/bidi-shaper/+esm\";\n\n  ctx.fillText(render(\"مرحبا بالعالم\"), x, y); // your own rasterizer, fixed\n</script>\n```\n\nAdapter subpaths are single files too — e.g. `https://cdn.jsdelivr.net/npm/bidi-shaper/dist/adapters/jspdf.js`. Pin a version for production (`bidi-shaper@0.1`).\n\n---\n\n## Quick start\n\n```ts\nimport {\n  render,              // the one-call pipeline: shape → reorder → mirror\n  analyze,             // render + embedding levels + visual↔logical index maps\n  shape,               // Arabic shaping only (logical order in, logical order out)\n  reorder,             // UAX #9 reordering + mirroring only, no shaping\n  getEmbeddingLevels,  // resolved level per code point (odd = RTL)\n  detectDirection,     // first-strong direction: 'ltr' | 'rtl' | 'neutral'\n  UNICODE_VERSION,     // the UCD version the tables were generated from\n} from \"bidi-shaper\";\n\nrender(\"مرحبا بالعالم\");        // 'ﻢﻟﺎﻌﻟﺎﺑ ﺎﺒﺣﺮﻣ'  shaped + reordered\nrender(\"سلام دنیا\");            // 'ﺎﯿﻧﺩ ﻡﻼﺳ'        Persian works the same\nrender(\"قیمت: 123.45\");         // '123.45 :ﺖﻤﯿﻗ'    numbers stay LTR\nrender(\"قائمة (أ)\");            // brackets mirrored correctly\nrender(\"hello world\");          // 'hello world'      ASCII fast path: returned as-is\n```\n\nEverything is configurable:\n\n```ts\nrender(text, {\n  direction: \"auto\",   // 'auto' | 'ltr' | 'rtl' — base paragraph direction (P2/P3 first-strong)\n  shape: true,         // Arabic presentation forms (default true)\n  ligatures: true,     // lam-alef ligatures (default true)\n  mirror: true,        // L4 bracket mirroring (default true)\n  tashkeel: \"keep\",    // 'keep' | 'strip' Arabic diacritics (default 'keep')\n  paragraphs: \"split\", // 'split' | 'single' — reorder each \\n-paragraph separately\n});\n```\n\n---\n\n## The problem in 30 seconds\n\nBrowsers, iOS/Android text views, and HarfBuzz-based stacks run two invisible passes before any glyph hits the screen:\n\n1. **The Unicode Bidirectional Algorithm (UAX #9)** — Arabic and other right-to-left scripts are *stored* in reading order (\"logical order\") but *drawn* right-to-left, with numbers and embedded Latin still flowing left-to-right. Something has to compute the final left-to-right glyph sequence (\"visual order\").\n2. **Arabic contextual shaping** — Arabic letters are cursive: the same letter takes a different form when it starts, continues, or ends a word (ع ﻋ ﻌ ﻊ are all one letter). Fonts handle this through shaping engines.\n\nRenderers that place glyphs one after another run **neither** pass. bidi-shaper runs both in pure TypeScript and hands you a plain string in final visual order. Draw it left-to-right, glyph by glyph, and it's right.\n\n```\n  logical-order string  (what you store: \"مرحبا بالعالم\")\n          │\n          ▼\n  ┌─────────────────────────────────────────────────────────┐\n  │ 1. Arabic shaping  (Unicode core spec §9.2)             │\n  │    joining classes → isolated/initial/medial/final      │\n  │    forms → lam-alef ligatures                           │\n  ├─────────────────────────────────────────────────────────┤\n  │ 2. UAX #9 Bidirectional Algorithm                       │\n  │    P1–P3   paragraph split + base direction             │\n  │    X1–X10  embeddings, overrides, isolates              │\n  │    W1–W7   weak types (numbers, separators, marks)      │\n  │    N0–N2   bracket pairs + neutrals                     │\n  │    I1–I2   implicit levels                              │\n  │    L1, L2  level resets + run reversal                  │\n  │    L4      character mirroring  ( ( ↔ ) )               │\n  └─────────────────────────────────────────────────────────┘\n          │\n          ▼\n  visual-order string  (what you draw, left to right: \"ﻢﻟﺎﻌﻟﺎﺑ ﺎﺒﺣﺮﻣ\")\n```\n\nShaping runs first, in logical order, because joining context is defined over logical neighbors; presentation forms keep the `AL` bidi class, so the reorder pass is unaffected. The whole pipeline is code-point based — surrogate-safe, emoji and astral characters count as one unit.\n\n---\n\n## API\n\n### `render(text, options?) → string`\n\nThe one-call pipeline. Returns the shaped, visual-order string. Plain-ASCII input under `direction: 'auto' | 'ltr'` is returned **by reference** (zero allocation) — mixed-content apps pay nothing for the common case.\n\n### `analyze(text, options?) → AnalyzeResult`\n\nEverything `render` does, plus the geometry interactive renderers need:\n\n```ts\nconst a = analyze(\"پa\");\na.text;             // 'aﭖ'   — visual-order output\na.direction;        // 'rtl'  — resolved base direction of the first paragraph\na.levels;           // Uint8Array [1, 2] — embedding level per INPUT code point (odd = RTL)\na.visualToLogical;  // [1, 0] — input index shown at each visual position\na.logicalToVisual;  // Int32Array [1, 0] — visual position of each input code point, -1 if removed\n```\n\nUse it for: mapping a click on glyph *i* back to the source character, drawing selection rectangles run-by-run, placing carets, underlining a logical range. Positions removed from the output (stripped tashkeel, the alef absorbed into a lam-alef ligature, explicit BiDi controls dropped by rule X9) map to `-1` in `logicalToVisual` and inherit the level of the character they attach to. All indices are **code point** indices, not UTF-16 units.\n\n### `shape(text, options?) → string`\n\nArabic shaping only — logical order in, logical order out. Useful when something else (e.g. an existing bidi pass) handles reordering. Options: `{ ligatures, tashkeel }`.\n\n### `reorder(text, options?) → string`\n\nUAX #9 reordering + mirroring only, no shaping. Options: `{ direction, mirror, paragraphs }`.\n\n### `getEmbeddingLevels(text, options?) → Uint8Array`\n\nResolved embedding level per code point (after L1). Odd levels render right-to-left. Options: `{ direction, paragraphs }`.\n\n### `detectDirection(text) → 'ltr' | 'rtl' | 'neutral'`\n\nFirst-strong detection per P2/P3 (isolate-skipping). `'neutral'` when no strong character exists — decide your own fallback.\n\n### `UNICODE_VERSION`\n\nThe UCD version the bundled tables were generated from (currently `17.0.0`).\n\n---\n\n## Adapters — wire it to your renderer\n\nEach adapter is a separate entry point and is **structurally typed** — it never imports the host library, so it adds nothing to your bundle beyond the engine itself.\n\n| Renderer | Import | One-liner |\n|---|---|---|\n| jsPDF | `bidi-shaper/jspdf` | `installJsPdfShaper(jsPDF.API)` — every `doc.text()` fixed automatically |\n| pdfmake | `bidi-shaper/pdfmake` | `shapeDocDefinition(def)` — deep-walks the whole document definition |\n| PDFKit | `bidi-shaper/pdfkit` | `textBidi(doc, text, x, y)` — also stops fontkit from re-shaping |\n| Canvas 2D | `bidi-shaper/canvas` | `fillTextBidi(ctx, text, x, y)` — per-line direction-aware alignment |\n| three.js | `bidi-shaper/three` | `prepareText(label)` — feed to `TextGeometry` / troika / bitmap text |\n| anything else | `bidi-shaper` | `render(text)` — the universal move |\n\n### jsPDF\n\n```ts\nimport { jsPDF } from \"jspdf\";\nimport { installJsPdfShaper, rtlText } from \"bidi-shaper/jspdf\";\n\n// Option A: install once, every doc.text() is processed automatically\ninstallJsPdfShaper(jsPDF.API);\n\n// Option B: per call\ndoc.text(rtlText(\"مرحبا بالعالم\"), 40, 60);\n```\n\n`installJsPdfShaper` registers a `preProcessText` plugin event. Output uses presentation forms (U+FB50–U+FEFF), which jsPDF's built-in arabic parser ignores — no double processing. Embed a font that contains those glyphs (Amiri, Noto Naskh Arabic, Cairo, most Arabic TTFs).\n\n### pdfmake\n\n```ts\nimport { shapeDocDefinition } from \"bidi-shaper/pdfmake\";\n\npdfMake.createPdf(\n  shapeDocDefinition(docDefinition, { rtlAlignment: true }),\n).download();\n```\n\nDeep-walks `content`/`header`/`footer` — strings, `text` nodes and arrays, `stack`, `columns`, `ul`/`ol`, and `table.body` cells — returning a new definition (input untouched). `rtlAlignment: true` adds `alignment: 'right'` to RTL text nodes that don't set their own.\n\n### PDFKit\n\n```ts\nimport PDFDocument from \"pdfkit\";\nimport { textBidi } from \"bidi-shaper/pdfkit\";\n\ntextBidi(doc, \"مرحبا بالعالم\", 72, 80, { align: \"right\" });\ntextBidi(doc, \"سلام\", { align: \"right\", bidi: { direction: \"rtl\" } }); // (text, options) form works too\n```\n\nPDFKit is a special case: its font engine (fontkit) **does** shape Arabic but does **no** BiDi — and reordering first would feed fontkit a mirrored joining context. `textBidi` therefore shapes + reorders here and passes `features: []` so fontkit doesn't re-substitute (supply your own `features` array to override). Regular PDFKit options (`align`, `width`, …) pass through; shaping options go under the `bidi` key.\n\n### Canvas 2D\n\nFor canvas implementations that don't shape — custom rasterizers, bitmap-font engines, some embedded/offscreen contexts (browser canvas shapes by itself):\n\n```ts\nimport { fillTextBidi, prepareCanvasText } from \"bidi-shaper/canvas\";\n\nfillTextBidi(ctx, \"سلام\\nworld\", x, y, { align: \"start\", lineHeight: 28 });\n// 'start'/'end' resolve per line direction: the RTL line anchors right, the LTR line left\n\nconst lines = prepareCanvasText(text); // [{ text, direction }, …] if you'd rather draw yourself\n```\n\n### three.js\n\n```ts\nimport { prepareText, prepareLines } from \"bidi-shaper/three\";\n\nnew TextGeometry(prepareText(\"مرحبا\"), { font, size: 1 });\n\nfor (const { text, direction } of prepareLines(label)) {\n  // direction tells you which edge to anchor each line to\n}\n```\n\nWorks the same for troika-three-text, BitmapText, SDF/MSDF text plugins — anything that places glyphs in string order.\n\n---\n\n## Recipes\n\n### An Arabic invoice in jsPDF\n\n```ts\nimport { jsPDF } from \"jspdf\";\nimport { installJsPdfShaper } from \"bidi-shaper/jspdf\";\n\nconst doc = new jsPDF();\ndoc.addFont(\"Amiri-Regular.ttf\", \"Amiri\", \"normal\");\ndoc.setFont(\"Amiri\");\ninstallJsPdfShaper(jsPDF.API);          // one line — every string below just works\n\ndoc.text(\"فاتورة ضريبية\", 200, 20, { align: \"right\" });\ndoc.text(\"الإجمالي: 1,250.00 ريال\", 200, 40, { align: \"right\" });\ndoc.save(\"invoice.pdf\");\n```\n\n### A server-rendered PDF (Node + PDFKit)\n\n```ts\nimport PDFDocument from \"pdfkit\";\nimport { textBidi } from \"bidi-shaper/pdfkit\";\n\nconst doc = new PDFDocument();\ndoc.pipe(res);\ndoc.font(\"fonts/NotoNaskhArabic-Regular.ttf\").fontSize(16);\ntextBidi(doc, \"تقرير المبيعات — سنة ١٤٤٧\", { align: \"right\" });\ndoc.end();\n```\n\n### Hit-testing a custom text editor / canvas UI\n\n```ts\nimport { analyze } from \"bidi-shaper\";\n\nconst a = analyze(sourceText);\n// draw a.text glyph-by-glyph, remembering each glyph's x-position…\nconst clickedVisual = xToGlyphIndex(clickX);\nconst sourceIndex = a.visualToLogical[clickedVisual]; // caret goes HERE in the stored string\n```\n\n---\n\n## Conformance — all 861,948 official cases\n\nThe complete official Unicode 17.0.0 test suites run in CI on every commit. Not a sample — the whole thing, both suites, pinned at 100%:\n\n| Suite | What it covers | Cases | Status |\n|---|---|---:|---|\n| [`BidiTest.txt`](https://www.unicode.org/Public/UCD/latest/ucd/BidiTest.txt) | All Bidi_Class sequences up to length 4 + known-pitfall cases, under auto/LTR/RTL | 770,241 | ✅ all pass |\n| [`BidiCharacterTest.txt`](https://www.unicode.org/Public/UCD/latest/ucd/BidiCharacterTest.txt) | Real code-point sequences including paired-bracket resolution (N0/BD16) | 91,707 | ✅ all pass |\n\n```sh\nnpm run test:conformance\n```\n\nGzipped fixtures are committed, so the suite is hermetic — no network, no version drift. The full run completes in a few seconds.\n\n**One deliberate deviation, in the string API only:** strict L1+L2 would reverse a trailing paragraph separator (`\\n`) to the visual *front* of an RTL line. `render()`/`analyze()` keep separators at their logical positions so line structure survives (`'سلام\\nabc'` → `'ﻡﻼﺳ\\nabc'`, not `'\\nﻡﻼﺳabc'`) — the behavior every practical consumer expects and what fribidi-style `log2vis` APIs do. The conformance harness exercises the spec-pure code path.\n\n---\n\n## Engineering\n\n| | |\n|---|---|\n| **Dependencies** | Zero runtime dependencies — no WASM, no native modules, no fonts |\n| **Size** | ~15 kB min+gzip including all Unicode tables |\n| **Formats** | Dual ESM + CJS, full `.d.ts` / `.d.cts` types, all six entry points |\n| **Type resolution** | Green across `node10` / `node16` / `bundler` on [arethetypeswrong](https://arethetypeswrong.github.io/) — subpath types work even on legacy `moduleResolution` |\n| **Tree-shaking** | `\"sideEffects\": false` — adapters never import their host library |\n| **Correctness** | 861,948 official UAX #9 conformance cases + unit suite, on every commit |\n| **Data source** | Generated from the UCD (`DerivedBidiClass`, `BidiMirroring`, `BidiBrackets`, `ArabicShaping`, `UnicodeData`) — committed and reviewed like source |\n| **Platforms** | Node ≥ 18, all evergreen browsers, Deno, Bun, workers — anywhere strings exist |\n| **Surrogate-safe** | Code-point based throughout; emoji and astral characters count as one unit |\n\n### Performance\n\n- Property lookups are binary searches over flat `[start, end, value]` range tables (529 joining ranges, ~1k bidi-class ranges) — no megabyte lookup arrays, no Map allocations at query time.\n- Levels, classes, and flags live in `Uint8Array`s; the hot loops are monomorphic.\n- Pure-ASCII strings short-circuit across the whole API: `render(s) === s` (same reference), `analyze`/`getEmbeddingLevels` return zeros/identity without running the algorithm — ~0.3 µs for the overwhelmingly common case in mixed-content apps.\n- Real RTL text processes at roughly **150–250k short strings/sec**, ~10k ops/sec for 600-character Arabic paragraphs including shaping (Node 22, one laptop core).\n- `npm run bench` runs the suite (tinybench).\n\n---\n\n## Data tables & regeneration\n\nAll Unicode data is generated into `src/data/generated/` (committed, reviewed like source) from the UCD:\n\n```sh\nnpm run generate-data   # downloads UCD files into scripts/.cache, regenerates tables + fixtures\n```\n\nBumping to a new Unicode version is a one-command change followed by the conformance suite.\n\n---\n\n## FAQ\n\n**Do I still need a special font?**\nYes — bidi-shaper selects *which* glyph to draw (e.g. ﻌ instead of ع), but the font must contain Arabic Presentation Forms-A/B (U+FB50–U+FEFF). Amiri, Noto Naskh/Sans Arabic, Cairo, Tajawal, and most Arabic TTFs do. The library ships no fonts.\n\n**When should I *not* use this?**\nWhen a real shaping engine is available: browser DOM/CSS, native text views, HarfBuzz (e.g. `harfbuzzjs`), skia-canvas, node-canvas with Pango. Those produce typographically better results (cursive joining via OpenType, kashida justification, mark positioning). bidi-shaper is for environments where that machinery doesn't exist or costs too much — a HarfBuzz WASM build is ~1 MB; this is ~15 kB.\n\n**Which scripts are covered?**\nBiDi reordering: every RTL script (Arabic, Hebrew, Syriac, Thaana, N'Ko, …) — reordering is script-agnostic. Contextual shaping: the Arabic script (Arabic, Persian, Urdu, Kurdish, …), because Unicode only defines presentation forms for Arabic. Syriac/N'Ko cursive shaping needs OpenType, i.e. a real shaping engine.\n\n**What about kashida justification, full ligature sets, mark positioning?**\nOut of scope — those are font-level (OpenType) features. You get the standard presentation forms plus the four mandatory lam-alef ligatures, which is exactly what classic Arabic PDF/terminal pipelines use.\n\n**Are ZWJ / ZWNJ honored?**\nYes: ZWNJ (U+200C) breaks joining (Persian needs this constantly), ZWJ (U+200D) forces it, tatweel (U+0640) joins both sides. Harakat are transparent to joining and survive shaping — or strip them with `tashkeel: 'strip'` if your renderer can't position combining marks.\n\n**Why does the output look \"backwards\" in my editor?**\nBecause it *is* — the output is visual order, and your editor applies its own bidi pass on top, double-reversing it. Judge the output where it will be drawn (the PDF, the canvas), not in a text editor. The [demo](https://bidi-shaper.vercel.app) renders both honestly.\n\n**How do I get correct Arabic in jsPDF?**\n`npm install bidi-shaper`, then `installJsPdfShaper(jsPDF.API)` once — every `doc.text()` call is fixed automatically. Embed an Arabic font (Amiri, Noto Naskh). [Details ↑](#jspdf)\n\n---\n\n## 🕹 Live demo\n\n**[bidi-shaper.vercel.app](https://bidi-shaper.vercel.app)** — an interactive instrument, computed live in your browser by the library source. Step your own text through the actual algorithm phases — shape, level, the L2 reordering cascade (deepest runs reverse first), mirror — and watch a naive renderer draw the before/after. Embedding levels, visual↔logical routing, contextual forms, all live.\n\nRun it locally:\n\n```sh\ncd demo && npm install && npm run dev    # browser demo (Vite)\nnpm run build && node demo/terminal.mjs  # terminal demo\n```\n\n---\n\n## Development\n\n```sh\nnpm ci\nnpm test                 # unit + full conformance (~5 s)\nnpm run test:coverage    # enforces ≥90% on the algorithm core\nnpm run typecheck\nnpm run lint\nnpm run build            # ESM + CJS + .d.ts via tsup\nnpm run generate-data    # regenerate Unicode tables from the UCD\n```\n\nThe UAX #9 core lives in `src/bidi/` (`levels.ts` = X/W/N/I rules, `reorder.ts` = L1/L2), shaping in `src/shape/`, the public API in `src/api/`, generated tables in `src/data/generated/`. See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines — correctness reports citing a UAX #9 rule or Unicode test case are especially welcome.\n\n---\n\n## Contributing\n\nIssues and pull requests are welcome on [GitHub](https://github.com/cc1a2b/bidi-shaper/issues). Security reports: see [SECURITY.md](./SECURITY.md).\n\n---\n\n## License\n\n[MIT](./LICENSE) — free for commercial and personal use. Unicode data files © Unicode, Inc., used under the [Unicode License](https://www.unicode.org/license.txt).\n\n---\n\n## Author & more projects\n\nBuilt and maintained by **[cc1a2b](https://github.com/cc1a2b)**.\n\nIf bidi-shaper saves you time, please **[⭐ star it on GitHub](https://github.com/cc1a2b/bidi-shaper)** — it helps other developers find it. You might also like **[arabicfmt](https://github.com/cc1a2b/arabicfmt)** — Arabic-first formatting (currency, Hijri dates, تفقيط, plurals) from the same author — or explore [other open-source projects](https://github.com/cc1a2b?tab=repositories).\n\n<div align=\"center\">\n<sub>Built for wherever your glyphs land · <bdi lang=\"ar\">وقفٌ للمطوّرين</bdi></sub>\n</div>\n","readmeFilename":"README.md"}