Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,26 @@ An [Emscripten] port of a subset of the functionality of [International Componen

## Using @mapwhit/rtl-text

@mapwhit/rtl-text exposes two functions:
```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)`

Expand All @@ -19,6 +38,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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 3 additions & 8 deletions src/browser.js
Original file line number Diff line number Diff line change
@@ -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);
}
11 changes: 2 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -248,12 +247,6 @@ export default async function makeModule() {
return lines;
}

globalThis.registerRTLTextPlugin?.({
applyArabicShaping,
processBidirectionalText,
processStyledBidirectionalText
});

return {
applyArabicShaping,
processBidirectionalText,
Expand Down
8 changes: 3 additions & 5 deletions src/node.js
Original file line number Diff line number Diff line change
@@ -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);
}
32 changes: 32 additions & 0 deletions test/browser.js
Original file line number Diff line number Diff line change
@@ -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(), []);
});
});
2 changes: 1 addition & 1 deletion test.js → test/index.js
Original file line number Diff line number Diff line change
@@ -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();

Expand Down
24 changes: 24 additions & 0 deletions test/node.js
Original file line number Diff line number Diff line change
@@ -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'));
});
});
Loading