From 233939e9aa95f2e62b48fe2c6151cefad6cbcbab Mon Sep 17 00:00:00 2001 From: melitele Date: Sat, 24 Jan 2026 22:42:10 -0700 Subject: [PATCH 1/6] add description of `processStyledBidirectionalText` to readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1bf163b..8ea3d02 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ An [Emscripten] port of a subset of the functionality of [International Componen ## Using @mapwhit/rtl-text -@mapwhit/rtl-text exposes two functions: +@mapwhit/rtl-text exposes following functions: ### `applyArabicShaping(unicodeInput)` @@ -19,6 +19,11 @@ Takes an input string in "logical order" (i.e. characters in the order they are Takes an input string with characters in "logical order", along with a set of chosen line break points, and applies the [Unicode Bidirectional Algorithm] to the string. Returns an ordered set of lines with characters in "visual order" (i.e. characters in the order they are displayed, left-to-right). The algorithm will insert mandatory line breaks (`\n` etc.) if they are not already included in `lineBreakPoints`. +### `processStyledBidirectionalText(unicodeInput, styleIndices, lineBreakPoints)` + +Takes an input string in logical order and applies the BiDi algorithm using the chosen line break points to generate a set of lines with the characters re-arranged into visual order. + +Also takes an array of "style indices" that specify different styling on the input characters (the styles are represented as integers here, the caller is responsible for the actual implementation of styling). BiDi can both reorder and add/remove characters from the input string, but this function copies style information from the "source" logical characters to their corresponding visual characters in the output. [tilerenderer]: https://npmjs.org/package/@mapwhit/tilerenderer [mapbox-gl-rtl-text]: https://npmjs.org/package/@mapbox/mapbox-gl-rtl-text From 38b4e5ce50288a21ff28285ff415e2bd508d24e1 Mon Sep 17 00:00:00 2001 From: melitele Date: Sun, 25 Jan 2026 01:22:07 -0700 Subject: [PATCH 2/6] remove exposing public methods via `global` as unnecessary trick --- src/index.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/index.js b/src/index.js index 05f9c16..1674bbe 100644 --- a/src/index.js +++ b/src/index.js @@ -248,12 +248,6 @@ export default async function makeModule() { return lines; } - globalThis.registerRTLTextPlugin?.({ - applyArabicShaping, - processBidirectionalText, - processStyledBidirectionalText - }); - return { applyArabicShaping, processBidirectionalText, From dd9d4d984eb9971315328378d80837c17ba1f6b4 Mon Sep 17 00:00:00 2001 From: melitele Date: Sun, 25 Jan 2026 19:42:17 -0700 Subject: [PATCH 3/6] move tests to the `test` folder --- test.js => test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test.js => test/index.js (98%) diff --git a/test.js b/test/index.js similarity index 98% rename from test.js rename to test/index.js index 1d8cfa4..1741970 100644 --- a/test.js +++ b/test/index.js @@ -1,7 +1,7 @@ import assert from 'node:assert'; import test from 'node:test'; -import rtlText from './src/index.js'; +import rtlText from '../src/index.js'; const { applyArabicShaping, processBidirectionalText, processStyledBidirectionalText } = await rtlText(); From 76bf723e0bce04f59985ceab7d8bea0abec92b37 Mon Sep 17 00:00:00 2001 From: melitele Date: Sun, 25 Jan 2026 20:05:26 -0700 Subject: [PATCH 4/6] `undici` for mock `fetch` --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5269727..78fe442 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "prepare": "make build" }, "devDependencies": { - "@biomejs/biome": "2.2.4" + "@biomejs/biome": "2.2.4", + "undici": "^7.19.1" }, "files": [ "src/*.js", From 0db83adff2620ac6236e81a6ecffe439ec3816a8 Mon Sep 17 00:00:00 2001 From: melitele Date: Sun, 25 Jan 2026 15:33:50 -0700 Subject: [PATCH 5/6] pass location of the wasm file as function parameter remove looking for it in the element on the page --- src/browser.js | 11 +++-------- src/index.js | 5 ++--- src/node.js | 8 +++----- test/browser.js | 32 ++++++++++++++++++++++++++++++++ test/node.js | 24 ++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 test/browser.js create mode 100644 test/node.js diff --git a/src/browser.js b/src/browser.js index 1c18b95..3d9d425 100644 --- a/src/browser.js +++ b/src/browser.js @@ -1,14 +1,9 @@ function locateFile(file) { - return new URL(urlFromDataset(file) ?? file, document.baseURI); + return new URL(file, document.baseURI); } -function urlFromDataset(file) { - const el = document.getElementById('rtl-text'); - return el?.getAttribute(`data-${file.replace('.', '-')}`); -} - -export default async function instantiateAsync(imports) { - const url = locateFile('icu.wasm'); +export default async function instantiateAsync(imports, file = 'icu.wasm') { + const url = locateFile(file); const response = fetch(url, { mode: 'cors', credentials: 'omit' }); return await WebAssembly.instantiateStreaming(response, imports); } diff --git a/src/index.js b/src/index.js index 1674bbe..8611ef4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,8 @@ import instantiateAsync from '#instantiate'; import icu from './icu.js'; -export default async function makeModule() { - const Module = await icu({}, { instantiateAsync }); - +export default async function makeModule(file) { + const Module = await icu({}, { instantiateAsync: imports => instantiateAsync(imports, file) }); /** * Takes logical input and replaces Arabic characters with the "presentation form" * of their initial/medial/final forms, based on their order in the input. diff --git a/src/node.js b/src/node.js index 7c4a43d..aaae186 100644 --- a/src/node.js +++ b/src/node.js @@ -1,9 +1,7 @@ -import { readFile } from 'node:fs/promises'; +import fs from 'node:fs/promises'; import { resolve } from 'node:path'; -const filename = resolve(import.meta.dirname, './icu.wasm'); - -export default async function instantiate(imports) { - const buffer = await readFile(filename); +export default async function instantiateAsync(imports, file = './icu.wasm') { + const buffer = await fs.readFile(resolve(import.meta.dirname, file)); return await WebAssembly.instantiate(buffer, imports); } diff --git a/test/browser.js b/test/browser.js new file mode 100644 index 0000000..3c3c805 --- /dev/null +++ b/test/browser.js @@ -0,0 +1,32 @@ +import test from 'node:test'; +import { MockAgent, setGlobalDispatcher } from 'undici'; +import instantiateAsync from '../src/browser.js'; + +const url = 'http://example.com'; +const mockAgent = new MockAgent(); + +test('instantiate in browser', async t => { + t.before(() => { + global.document = {}; + global.document.baseURI = url; + setGlobalDispatcher(mockAgent); + mockAgent.disableNetConnect(); + }); + t.after(async () => { + await mockAgent.close(); + }); + + await t.test('default location', async t => { + mockAgent.get(url).intercept({ path: '/icu.wasm' }).reply(404); + + await t.assert.rejects(instantiateAsync({})); + t.assert.deepEqual(mockAgent.pendingInterceptors(), []); + }); + + await t.test('provided location', async t => { + mockAgent.get(url).intercept({ path: '/icu-123.wasm' }).reply(404); + + await t.assert.rejects(instantiateAsync({}, 'icu-123.wasm')); + t.assert.deepEqual(mockAgent.pendingInterceptors(), []); + }); +}); diff --git a/test/node.js b/test/node.js new file mode 100644 index 0000000..0476da3 --- /dev/null +++ b/test/node.js @@ -0,0 +1,24 @@ +import fs from 'node:fs/promises'; +import test from 'node:test'; +import instantiateAsync from '../src/node.js'; + +test('instantiate in node', async t => { + t.before(() => {}); + t.after(async () => {}); + + await t.test('default location', async t => { + t.mock.method(fs, 'readFile', () => Promise.reject()); + + await t.assert.rejects(instantiateAsync({})); + t.assert.equal(fs.readFile.mock.callCount(), 1); + t.assert.ok(fs.readFile.mock.calls[0].arguments[0].endsWith('/icu.wasm')); + }); + + await t.test('provided location', async t => { + t.mock.method(fs, 'readFile', () => Promise.reject()); + + await t.assert.rejects(instantiateAsync({}, './icu-123.wasm')); + t.assert.equal(fs.readFile.mock.callCount(), 1); + t.assert.ok(fs.readFile.mock.calls[0].arguments[0].endsWith('/icu-123.wasm')); + }); +}); From dafbbe668cb87f3393be177b806292f6a30fce57 Mon Sep 17 00:00:00 2001 From: melitele Date: Mon, 26 Jan 2026 16:27:01 -0700 Subject: [PATCH 6/6] add how to use --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 8ea3d02..d09f09b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,25 @@ An [Emscripten] port of a subset of the functionality of [International Componen ## Using @mapwhit/rtl-text +```js +import rtlText from '@mapwhit/rtl-text'; +const {applyArabicShaping, processBidirectionalText} = await rtlText(); + +const arabicString = "سلام"; +const shapedArabicText = applyArabicShaping(arabicString); +const readyForDisplay = processBidirectionalText(shapedArabicText, []); +``` + +The default location of the compiled wasm file is the same folder as the javascript. If different it can be provided as a parameter to `rtlText`. + +```js +// in browser +const {applyArabicShaping, processBidirectionalText} = await rtlText('https://example.com/rtl/icu-123.wasm'); + +// in node +const {applyArabicShaping, processBidirectionalText} = await rtlText('/lib/rtl/icu-123.wasm'); +``` + @mapwhit/rtl-text exposes following functions: ### `applyArabicShaping(unicodeInput)`