Skip to content

Commit a2f1450

Browse files
authored
Support for RTL text plugin 0.3.0 (#4860)
* Allow loading rtl plugin async * Allow loading rtl plugin async * Add changelog * Fix typecheck and code * Add tests to make sure both 0.2.3 and 0.3.0 are supported * Improve test name * Change to use latest version everywhere. * Revert unrelated changes
1 parent 904d372 commit a2f1450

9 files changed

Lines changed: 181 additions & 139 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### 🐞 Bug fixes
88
- Fix text not being hidden behind the globe when overlap mode was set to `always` ([#4802](https://github.com/maplibre/maplibre-gl-js/issues/4802))
99
- Fix a single white frame being displayed when the map internally transitions from mercator to globe projection ([#4816](https://github.com/maplibre/maplibre-gl-js/issues/4816))
10+
- Fix loading of RTL plugin version 0.3.0 ([#4860](https://github.com/maplibre/maplibre-gl-js/pull/4860))
1011
- _...Add new stuff here..._
1112

1213
## 5.0.0-pre.2

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export type * from '@maplibre/maplibre-gl-style-spec';
6060
* rtl text will then be rendered only after the plugin finishes loading.
6161
* @example
6262
* ```ts
63-
* setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.js', false);
63+
* setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.3.0/dist/mapbox-gl-rtl-text.js', false);
6464
* ```
6565
* @see [Add support for right-to-left scripts](https://maplibre.org/maplibre-gl-js/docs/examples/mapbox-gl-rtl-text/)
6666
*/
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {PluginState} from './rtl_text_plugin_status';
2+
import {rtlWorkerPlugin} from './rtl_text_plugin_worker';
3+
4+
describe('RTLWorkerPlugin', () => {
5+
beforeEach(() => {
6+
// This is a static class, so we need to reset the properties before each test
7+
rtlWorkerPlugin.processStyledBidirectionalText = null;
8+
rtlWorkerPlugin.processBidirectionalText = null;
9+
rtlWorkerPlugin.applyArabicShaping = null;
10+
});
11+
12+
test('should throw if already parsed', () => {
13+
const rtlTextPlugin = {
14+
applyArabicShaping: jest.fn(),
15+
processBidirectionalText: jest.fn(),
16+
processStyledBidirectionalText: jest.fn(),
17+
};
18+
19+
rtlWorkerPlugin.setMethods(rtlTextPlugin);
20+
expect(() => {
21+
rtlWorkerPlugin.setMethods(rtlTextPlugin);
22+
}).toThrow('RTL text plugin already registered.');
23+
});
24+
25+
test('should move RTL plugin from unavailable to deferred', async () => {
26+
rtlWorkerPlugin.pluginURL = '';
27+
rtlWorkerPlugin.pluginStatus = 'unavailable';
28+
29+
const mockMessage: PluginState = {
30+
pluginURL: 'https://somehost/somescript',
31+
pluginStatus: 'deferred'
32+
};
33+
34+
await rtlWorkerPlugin.syncState(mockMessage, jest.fn());
35+
36+
expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('deferred');
37+
});
38+
39+
test('should not change RTL plugin status if already parsed', async () => {
40+
const originalUrl = 'https://somehost/somescript1';
41+
rtlWorkerPlugin.pluginURL = originalUrl;
42+
rtlWorkerPlugin.pluginStatus = 'loaded';
43+
rtlWorkerPlugin.setMethods({
44+
applyArabicShaping: jest.fn(),
45+
processBidirectionalText: jest.fn(),
46+
processStyledBidirectionalText: jest.fn(),
47+
});
48+
const mockMessage: PluginState = {
49+
pluginURL: 'https://somehost/somescript2',
50+
pluginStatus: 'loading'
51+
};
52+
53+
const workerResult: PluginState = await await rtlWorkerPlugin.syncState(mockMessage, jest.fn());
54+
55+
expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded');
56+
expect(rtlWorkerPlugin.pluginURL).toBe(originalUrl);
57+
58+
expect(workerResult.pluginStatus).toBe('loaded');
59+
expect(workerResult.pluginURL).toBe(originalUrl);
60+
});
61+
62+
test('should do a full cycle of rtl loading synchronously', async () => {
63+
const originalUrl = 'https://somehost/somescript1';
64+
const loadScriptsMock = jest.fn().mockImplementation((_) => {
65+
rtlWorkerPlugin.setMethods({
66+
applyArabicShaping: jest.fn(),
67+
processBidirectionalText: jest.fn(),
68+
processStyledBidirectionalText: jest.fn(),
69+
});
70+
});
71+
72+
const workerResult: PluginState = await rtlWorkerPlugin.syncState({
73+
pluginURL: originalUrl,
74+
pluginStatus: 'loading'
75+
}, loadScriptsMock);
76+
77+
expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded');
78+
expect(rtlWorkerPlugin.pluginURL).toBe(originalUrl);
79+
expect(workerResult.pluginStatus).toBe('loaded');
80+
expect(workerResult.pluginURL).toBe(originalUrl);
81+
});
82+
83+
test('should do a full cycle of rtl loading asynchronously', async () => {
84+
const originalUrl = 'https://somehost/somescript1';
85+
const loadScriptsMock = jest.fn().mockImplementation((_) => {
86+
setTimeout(() => {
87+
rtlWorkerPlugin.setMethods({
88+
applyArabicShaping: jest.fn(),
89+
processBidirectionalText: jest.fn(),
90+
processStyledBidirectionalText: jest.fn(),
91+
});
92+
}, 10);
93+
});
94+
95+
const workerResult: PluginState = await rtlWorkerPlugin.syncState({
96+
pluginURL: originalUrl,
97+
pluginStatus: 'loading'
98+
}, loadScriptsMock);
99+
100+
expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded');
101+
expect(rtlWorkerPlugin.pluginURL).toBe(originalUrl);
102+
expect(workerResult.pluginStatus).toBe('loaded');
103+
expect(workerResult.pluginURL).toBe(originalUrl);
104+
});
105+
106+
test('should fail loading on timeout', async () => {
107+
const originalUrl = 'https://somehost/somescript1';
108+
const loadScriptsMock = jest.fn().mockImplementation(() => {});
109+
110+
(rtlWorkerPlugin as any).TIMEOUT = 1;
111+
112+
await expect(rtlWorkerPlugin.syncState({
113+
pluginURL: originalUrl,
114+
pluginStatus: 'loading'
115+
}, loadScriptsMock)
116+
).rejects.toThrow('RTL Text Plugin failed to import scripts from https://somehost/somescript1');
117+
});
118+
});

src/source/rtl_text_plugin_worker.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,81 @@ export interface RTLTextPlugin {
77
}
88

99
class RTLWorkerPlugin implements RTLTextPlugin {
10+
readonly TIMEOUT = 5000;
11+
1012
applyArabicShaping: (a: string) => string = null;
1113
processBidirectionalText: ((b: string, a: Array<number>) => Array<string>) = null;
1214
processStyledBidirectionalText: ((c: string, b: Array<number>, a: Array<number>) => Array<[string, Array<number>]>) = null;
1315
pluginStatus: RTLPluginStatus = 'unavailable';
1416
pluginURL: string = null;
17+
loadScriptResolve: () => void = () => {};
1518

16-
setState(state: PluginState) {
19+
private setState(state: PluginState) {
1720
this.pluginStatus = state.pluginStatus;
1821
this.pluginURL = state.pluginURL;
1922
}
2023

21-
getState(): PluginState {
24+
private getState(): PluginState {
2225
return {
2326
pluginStatus: this.pluginStatus,
2427
pluginURL: this.pluginURL
2528
};
2629
}
2730

28-
setMethods(rtlTextPlugin: RTLTextPlugin) {
31+
public setMethods(rtlTextPlugin: RTLTextPlugin) {
32+
if (rtlWorkerPlugin.isParsed()) {
33+
throw new Error('RTL text plugin already registered.');
34+
}
2935
this.applyArabicShaping = rtlTextPlugin.applyArabicShaping;
3036
this.processBidirectionalText = rtlTextPlugin.processBidirectionalText;
3137
this.processStyledBidirectionalText = rtlTextPlugin.processStyledBidirectionalText;
38+
this.loadScriptResolve();
3239
}
3340

34-
isParsed(): boolean {
41+
public isParsed(): boolean {
3542
return this.applyArabicShaping != null &&
3643
this.processBidirectionalText != null &&
3744
this.processStyledBidirectionalText != null;
3845
}
3946

40-
getPluginURL(): string {
41-
return this.pluginURL;
47+
public getRTLTextPluginStatus() {
48+
return this.pluginStatus;
4249
}
4350

44-
getRTLTextPluginStatus() {
45-
return this.pluginStatus;
51+
public async syncState(incomingState: PluginState, importScripts: (url: string) => void): Promise<PluginState> {
52+
// Parsed plugin cannot be changed, so just return its current state.
53+
if (this.isParsed()) {
54+
return this.getState();
55+
}
56+
57+
if (incomingState.pluginStatus !== 'loading') {
58+
// simply sync and done
59+
this.setState(incomingState);
60+
return incomingState;
61+
}
62+
const urlToLoad = incomingState.pluginURL;
63+
const loadScriptPromise = new Promise<void>((resolve) => {
64+
this.loadScriptResolve = resolve;
65+
});
66+
importScripts(urlToLoad);
67+
const dontWaitForeverTimeoutPromise = new Promise<void>((resolve) => setTimeout(() => resolve(), this.TIMEOUT));
68+
await Promise.race([loadScriptPromise, dontWaitForeverTimeoutPromise]);
69+
const complete = this.isParsed();
70+
if (complete) {
71+
const loadedState: PluginState = {
72+
pluginStatus: 'loaded',
73+
pluginURL: urlToLoad
74+
};
75+
this.setState(loadedState);
76+
return loadedState;
77+
}
78+
79+
// error case
80+
this.setState({
81+
pluginStatus: 'error',
82+
pluginURL: ''
83+
});
84+
throw new Error(`RTL Text Plugin failed to import scripts from ${urlToLoad}`);
4685
}
4786
}
4887

src/source/worker.test.ts

Lines changed: 8 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {CanonicalTileID, OverscaledTileID} from './tile_id';
66
import {WorkerSource, WorkerTileParameters, WorkerTileResult} from './worker_source';
77
import {rtlWorkerPlugin} from './rtl_text_plugin_worker';
88
import {ActorTarget, IActor} from '../util/actor';
9-
import {PluginState} from './rtl_text_plugin_status';
109
import {MessageType} from '../util/actor_messages';
1110

1211
class WorkerSourceMock implements WorkerSource {
@@ -37,108 +36,22 @@ describe('Worker RTLTextPlugin', () => {
3736
} as any;
3837
worker = new Worker(_self);
3938
global.fetch = null;
40-
rtlWorkerPlugin.setMethods({
41-
applyArabicShaping: null,
42-
processBidirectionalText: null,
43-
processStyledBidirectionalText: null
44-
});
45-
jest.spyOn(rtlWorkerPlugin, 'isParsed').mockImplementation(() => {
46-
return false;
47-
});
48-
});
49-
50-
test('should not throw and set values in plugin', () => {
51-
const rtlTextPlugin = {
52-
applyArabicShaping: 'test',
53-
processBidirectionalText: 'test',
54-
processStyledBidirectionalText: 'test',
55-
};
56-
57-
_self.registerRTLTextPlugin(rtlTextPlugin);
58-
expect(rtlWorkerPlugin.applyArabicShaping).toBe('test');
59-
expect(rtlWorkerPlugin.processBidirectionalText).toBe('test');
60-
expect(rtlWorkerPlugin.processStyledBidirectionalText).toBe('test');
61-
});
62-
63-
test('should throw if already parsed', () => {
64-
jest.spyOn(rtlWorkerPlugin, 'isParsed').mockImplementation(() => {
65-
return true;
66-
});
67-
68-
const rtlTextPlugin = {
69-
applyArabicShaping: jest.fn(),
70-
processBidirectionalText: jest.fn(),
71-
processStyledBidirectionalText: jest.fn(),
72-
};
73-
74-
expect(() => {
75-
_self.registerRTLTextPlugin(rtlTextPlugin);
76-
}).toThrow('RTL text plugin already registered.');
7739
});
7840

79-
test('should move RTL plugin from unavailable to deferred', async () => {
80-
rtlWorkerPlugin.setState({
81-
pluginURL: '',
82-
pluginStatus: 'unavailable'
83-
}
84-
);
85-
const mockMessage: PluginState = {
86-
pluginURL: 'https://somehost/somescript',
87-
pluginStatus: 'deferred'
88-
};
89-
90-
await worker.actor.messageHandlers[MessageType.syncRTLPluginState]('', mockMessage);
91-
expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('deferred');
92-
});
41+
test('should call setMethods in plugin', () => {
42+
const spy = jest.spyOn(rtlWorkerPlugin, 'setMethods').mockImplementation(() => {});
9343

94-
test('should download RTL plugin when "loading" message is received', async () => {
95-
rtlWorkerPlugin.setState({
96-
pluginURL: '',
97-
pluginStatus: 'deferred'
98-
});
44+
_self.registerRTLTextPlugin({} as any);
9945

100-
const mockURL = 'https://somehost/somescript';
101-
const mockMessage: PluginState = {
102-
pluginURL: mockURL,
103-
pluginStatus: 'loading'
104-
};
105-
106-
const importSpy = jest.spyOn(worker.self, 'importScripts').mockImplementation(() => {
107-
// after importing isParse() to return true
108-
jest.spyOn(rtlWorkerPlugin, 'isParsed').mockImplementation(() => {
109-
return true;
110-
});
111-
});
112-
113-
const syncResult: PluginState = await worker.actor.messageHandlers[MessageType.syncRTLPluginState]('', mockMessage) as any;
114-
expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded');
115-
expect(importSpy).toHaveBeenCalledWith(mockURL);
116-
117-
expect(syncResult.pluginURL).toBe(mockURL);
118-
expect(syncResult.pluginStatus).toBe('loaded');
46+
expect(spy).toHaveBeenCalled();
11947
});
12048

121-
test('should not change RTL plugin status if already parsed', async () => {
122-
const originalUrl = 'https://somehost/somescript1';
123-
rtlWorkerPlugin.setState({
124-
pluginURL: originalUrl,
125-
pluginStatus: 'loaded'
126-
});
127-
128-
jest.spyOn(rtlWorkerPlugin, 'isParsed').mockImplementation(() => {
129-
return true;
130-
});
131-
const mockMessage: PluginState = {
132-
pluginURL: 'https://somehost/somescript2',
133-
pluginStatus: 'loading'
134-
};
49+
test('should call syncState when rtl message is received', async () => {
50+
const syncStateSpy = jest.spyOn(rtlWorkerPlugin, 'syncState').mockImplementation((_, __) => Promise.resolve({} as any));
13551

136-
const workerResult: PluginState = await worker.actor.messageHandlers[MessageType.syncRTLPluginState]('', mockMessage) as any;
137-
expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded');
138-
expect(rtlWorkerPlugin.getPluginURL()).toBe(originalUrl);
52+
await worker.actor.messageHandlers[MessageType.syncRTLPluginState]('', {} as any) as any;
13953

140-
expect(workerResult.pluginStatus).toBe('loaded');
141-
expect(workerResult.pluginURL).toBe(originalUrl);
54+
expect(syncStateSpy).toHaveBeenCalled();
14255
});
14356
});
14457

src/source/worker.ts

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ export default class Worker {
8383

8484
// This is invoked by the RTL text plugin when the download via the `importScripts` call has finished, and the code has been parsed.
8585
this.self.registerRTLTextPlugin = (rtlTextPlugin: RTLTextPlugin) => {
86-
if (rtlWorkerPlugin.isParsed()) {
87-
throw new Error('RTL text plugin already registered.');
88-
}
86+
8987
rtlWorkerPlugin.setMethods(rtlTextPlugin);
9088
};
9189

@@ -191,35 +189,8 @@ export default class Worker {
191189
}
192190

193191
private async _syncRTLPluginState(mapId: string, incomingState: PluginState): Promise<PluginState> {
194-
195-
// Parsed plugin cannot be changed, so just return its current state.
196-
if (rtlWorkerPlugin.isParsed()) {
197-
return rtlWorkerPlugin.getState();
198-
}
199-
200-
if (incomingState.pluginStatus !== 'loading') {
201-
// simply sync and done
202-
rtlWorkerPlugin.setState(incomingState);
203-
return incomingState;
204-
}
205-
const urlToLoad = incomingState.pluginURL;
206-
this.self.importScripts(urlToLoad);
207-
const complete = rtlWorkerPlugin.isParsed();
208-
if (complete) {
209-
const loadedState: PluginState = {
210-
pluginStatus: 'loaded',
211-
pluginURL: urlToLoad
212-
};
213-
rtlWorkerPlugin.setState(loadedState);
214-
return loadedState;
215-
}
216-
217-
// error case
218-
rtlWorkerPlugin.setState({
219-
pluginStatus: 'error',
220-
pluginURL: ''
221-
});
222-
throw new Error(`RTL Text Plugin failed to import scripts from ${urlToLoad}`);
192+
const state = await rtlWorkerPlugin.syncState(incomingState, this.self.importScripts);
193+
return state;
223194
}
224195

225196
private _getAvailableImages(mapId: string) {

0 commit comments

Comments
 (0)