@mapwhit/tilerenderer 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.4.0",
2
+ "version": "1.5.0",
3
3
  "type": "module"
4
4
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mapwhit/tilerenderer",
3
3
  "description": "A WebGL interactive maps library",
4
- "version": "1.4.0",
4
+ "version": "1.5.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
@@ -1,11 +1,12 @@
1
1
  import { dist } from '@mapwhit/point-geometry';
2
2
  import { Formatted } from '@mapwhit/style-expressions';
3
3
  import { VectorTileFeature } from '@mapwhit/vector-tile';
4
+ import { rtlPlugin } from '../../source/rtl_text_plugin.js';
4
5
  import EvaluationParameters from '../../style/evaluation_parameters.js';
5
6
  import mergeLines from '../../symbol/mergelines.js';
6
7
  import { getSizeData } from '../../symbol/symbol_size.js';
7
8
  import transformText from '../../symbol/transform_text.js';
8
- import { allowsVerticalWritingMode } from '../../util/script_detection.js';
9
+ import { allowsVerticalWritingMode, stringContainsRTLText } from '../../util/script_detection.js';
9
10
  import { verticalizedCharacterMap } from '../../util/verticalize_punctuation.js';
10
11
  import {
11
12
  CollisionBoxLayoutArray,
@@ -44,6 +45,15 @@ export function addDynamicAttributes(dynamicLayoutVertexArray, p, angle) {
44
45
  dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle);
45
46
  }
46
47
 
48
+ function containsRTLText(formattedText) {
49
+ for (const section of formattedText.sections) {
50
+ if (stringContainsRTLText(section.text)) {
51
+ return true;
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+
47
57
  /**
48
58
  * Unlike other buckets, which simply implement #addFeature with type-specific
49
59
  * logic for (essentially) triangulating feature geometries, SymbolBucket
@@ -86,6 +96,7 @@ export default class SymbolBucket {
86
96
  this.pixelRatio = options.pixelRatio;
87
97
  this.sourceLayerIndex = options.sourceLayerIndex;
88
98
  this.hasPattern = false;
99
+ this.hasRTLText = false;
89
100
  this.sortKeyRanges = [];
90
101
 
91
102
  const layer = this.layers[0];
@@ -169,11 +180,16 @@ export default class SymbolBucket {
169
180
  // but plain string token evaluation skips that pathway so do the
170
181
  // conversion here.
171
182
  const resolvedTokens = layer.getValueAndResolveTokens('text-field', feature);
172
- text = transformText(
173
- resolvedTokens instanceof Formatted ? resolvedTokens : Formatted.fromString(resolvedTokens),
174
- layer,
175
- feature
176
- );
183
+ const formattedText =
184
+ resolvedTokens instanceof Formatted ? resolvedTokens : Formatted.fromString(resolvedTokens);
185
+ // on this instance: if hasRTLText is already true, all future calls to containsRTLText can be skipped.
186
+ const bucketHasRTLText = (this.hasRTLText = this.hasRTLText || containsRTLText(formattedText));
187
+ if (
188
+ !bucketHasRTLText || // non-rtl text so can proceed safely
189
+ rtlPlugin.isRTLSupported(true) // Use the rtlText plugin to shape text if available or We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping
190
+ ) {
191
+ text = transformText(formattedText, layer, feature);
192
+ }
177
193
  }
178
194
 
179
195
  let icon;
@@ -244,7 +260,9 @@ export default class SymbolBucket {
244
260
  }
245
261
 
246
262
  isEmpty() {
247
- return this.symbolInstances.length === 0;
263
+ // When the bucket encounters only rtl-text but the plugin isnt loaded, no symbol instances will be created.
264
+ // In order for the bucket to be serialized, and not discarded as an empty bucket both checks are necessary.
265
+ return this.symbolInstances.length === 0 && !this.hasRTLText;
248
266
  }
249
267
 
250
268
  uploadPending() {
package/src/index.js CHANGED
@@ -5,20 +5,21 @@ import { Point } from '@mapwhit/point-geometry';
5
5
  import packageJSON from '../package.json' with { type: 'json' };
6
6
  import { default as LngLat } from './geo/lng_lat.js';
7
7
  import { default as LngLatBounds } from './geo/lng_lat_bounds.js';
8
- import { setRTLTextPlugin } from './source/rtl_text_plugin.js';
8
+ import { rtlPluginLoader } from './source/rtl_text_plugin.js';
9
9
  import { default as Style } from './style/style.js';
10
10
  import { default as Map } from './ui/map.js';
11
11
  import config from './util/config.js';
12
12
 
13
13
  const { version } = packageJSON;
14
14
 
15
- export { version, config, setRTLTextPlugin, Point, LngLat, LngLatBounds, Style, Map, Evented };
15
+ export { version, config, setRTLTextPlugin, getRTLTextPluginStatus, Point, LngLat, LngLatBounds, Style, Map, Evented };
16
16
 
17
17
  // for commonjs backward compatibility
18
18
  const mapwhit = {
19
19
  version,
20
20
  config,
21
21
  setRTLTextPlugin,
22
+ getRTLTextPluginStatus,
22
23
  Point,
23
24
  LngLat,
24
25
  LngLatBounds,
@@ -27,24 +28,7 @@ const mapwhit = {
27
28
  Evented
28
29
  };
29
30
 
30
- const properties = {
31
- workerCount: {
32
- get() {
33
- return config.WORKER_COUNT;
34
- },
35
- set(count) {
36
- config.WORKER_COUNT = count;
37
- }
38
- },
39
- workerUrl: {
40
- get() {
41
- return config.WORKER_URL;
42
- },
43
- set(url) {
44
- config.WORKER_URL = url;
45
- }
46
- }
47
- };
31
+ const properties = {};
48
32
 
49
33
  Object.defineProperties(mapwhit, properties);
50
34
  Object.defineProperties(config, properties);
@@ -64,11 +48,27 @@ export default mapwhit;
64
48
  *
65
49
  * @function setRTLTextPlugin
66
50
  * @param {string} pluginURL URL pointing to the Mapbox RTL text plugin source.
67
- * @param {Function} callback Called with an error argument if there is an error.
51
+ * @param {boolean} lazy If set to `true`, mapboxgl will defer loading the plugin until rtl text is encountered,
52
+ * rtl text will then be rendered only after the plugin finishes loading.
53
+ * @example
54
+ * setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.3.0/dist/mapbox-gl-rtl-text.js', false);
55
+ * @see [Add support for right-to-left scripts](https://maplibre.org/maplibre-gl-js/docs/examples/mapbox-gl-rtl-text/)
56
+ */
57
+ function setRTLTextPlugin(pluginURL, lazy) {
58
+ return rtlPluginLoader.setRTLTextPlugin(pluginURL, lazy);
59
+ }
60
+ /**
61
+ * Gets the map's [RTL text plugin](https://www.mapbox.com/mapbox-gl-js/plugins/#mapbox-gl-rtl-text) status.
62
+ * The status can be `unavailable` (i.e. not requested or removed), `loading`, `loaded` or `error`.
63
+ * If the status is `loaded` and the plugin is requested again, an error will be thrown.
64
+ *
65
+ * @function getRTLTextPluginStatus
68
66
  * @example
69
- * mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js');
70
- * @see [Add support for right-to-left scripts](https://www.mapbox.com/mapbox-gl-js/example/mapbox-gl-rtl-text/)
67
+ * const pluginStatus = getRTLTextPluginStatus();
71
68
  */
69
+ function getRTLTextPluginStatus() {
70
+ return rtlPluginLoader.getRTLTextPluginStatus();
71
+ }
72
72
 
73
73
  // canary assert: used to confirm that asserts have been removed from production build
74
74
  import assert from 'assert';
@@ -1,82 +1,145 @@
1
+ import { Event, Evented } from '@mapwhit/events';
1
2
  import dynload from 'dynload';
2
3
  import browser from '../util/browser.js';
3
4
 
4
- let pluginRequested = false;
5
- let pluginURL;
6
- let loading = false;
5
+ /**
6
+ * The possible option of the plugin's status
7
+ *
8
+ * `unavailable`: Not loaded.
9
+ *
10
+ * `deferred`: The plugin URL has been specified, but loading has been deferred.
11
+ *
12
+ * `loading`: request in-flight.
13
+ *
14
+ * `loaded`: The plugin is now loaded
15
+ *
16
+ * `error`: The plugin failed to load
17
+ */
7
18
 
8
- let _completionCallback;
9
- const _loadedCallbacks = [];
10
-
11
- const rtlPlugin = {
12
- clearRTLTextPlugin, // exported for testing
13
- loadScript, // exported for testing
14
- registerForPluginAvailability,
15
- setRTLTextPlugin
16
- };
19
+ function RTLPlugin() {
20
+ const self = {
21
+ isRTLSupported
22
+ };
17
23
 
18
- export function registerForPluginAvailability(callback) {
19
- if (plugin.isLoaded()) {
20
- callback();
21
- return;
24
+ /**
25
+ * Checks whether the RTL language support is available.
26
+ * If `canChangeShortly` is false, it will only return true if the RTL language
27
+ * is properly supported.
28
+ * If `canChangeShortly` is true, it will also return true if the RTL language
29
+ * is not supported unless it can obtain the RTL text plugin.
30
+ * @param {boolean} canChangeShortly
31
+ * @returns
32
+ */
33
+ function isRTLSupported(canChangeShortly) {
34
+ if (rtlPluginLoader.getRTLTextPluginStatus() === 'loaded') {
35
+ return true;
36
+ }
37
+ if (!canChangeShortly) {
38
+ return false;
39
+ }
40
+ rtlPluginLoader.lazyLoad();
41
+ // any transitive state other than 'loading' means we can consider RTL supported as best as possible for now
42
+ return rtlPluginLoader.getRTLTextPluginStatus() !== 'loading';
22
43
  }
23
- _loadedCallbacks.push(callback);
24
- loadRTLTextPlugin();
25
- return () => _loadedCallbacks.splice(_loadedCallbacks.indexOf(callback), 1);
26
- }
27
44
 
28
- export function clearRTLTextPlugin() {
29
- _loadedCallbacks.length = 0;
30
- pluginRequested = false;
31
- pluginURL = undefined;
45
+ return self;
32
46
  }
33
47
 
34
- export function setRTLTextPlugin(url, callback) {
35
- if (pluginRequested) {
36
- throw new Error('setRTLTextPlugin cannot be called multiple times.');
48
+ export const rtlPlugin = RTLPlugin();
49
+
50
+ function RTLPluginLoader() {
51
+ let status = 'unavailable';
52
+ let url;
53
+
54
+ const self = {
55
+ getRTLTextPluginStatus,
56
+ setRTLTextPlugin,
57
+ lazyLoad,
58
+ _clearRTLTextPlugin,
59
+ _registerRTLTextPlugin
60
+ };
61
+
62
+ /** This one is exposed to outside */
63
+ function getRTLTextPluginStatus() {
64
+ return status;
65
+ }
66
+
67
+ // public for testing
68
+ function _clearRTLTextPlugin() {
69
+ url = undefined;
70
+ status = 'unavailable';
71
+ _setMethods();
37
72
  }
38
- pluginRequested = true;
39
- pluginURL = browser.resolveURL(url);
40
- _completionCallback = error => {
41
- if (error) {
42
- const msg = `RTL Text Plugin failed to load scripts from ${pluginURL}`;
43
- // Clear loaded state to allow retries
44
- clearRTLTextPlugin();
45
- if (callback) {
46
- callback(new Error(msg));
73
+
74
+ function setRTLTextPlugin(pluginURL, deferred = false) {
75
+ if (url) {
76
+ // error
77
+ return Promise.reject(new Error('setRTLTextPlugin cannot be called multiple times.'));
78
+ }
79
+ url = browser.resolveURL(pluginURL);
80
+ if (!url) {
81
+ return Promise.reject(new Error(`requested url ${pluginURL} is invalid`));
82
+ }
83
+ if (status === 'requested') {
84
+ return _downloadRTLTextPlugin();
85
+ }
86
+ if (status === 'unavailable') {
87
+ // from initial state:
88
+ if (!deferred) {
89
+ return _downloadRTLTextPlugin();
47
90
  }
91
+ status = 'deferred';
48
92
  }
49
- loading = false;
50
- _completionCallback = undefined;
51
- };
52
- loadRTLTextPlugin();
53
- }
93
+ }
54
94
 
55
- function loadRTLTextPlugin() {
56
- if (pluginURL && !plugin.isLoaded() && _loadedCallbacks.length > 0 && !loading) {
57
- // needs to be called as exported method for mock testing
58
- loading = rtlPlugin.loadScript(pluginURL).catch(_completionCallback);
95
+ async function _downloadRTLTextPlugin() {
96
+ status = 'loading';
97
+ try {
98
+ await rtlPluginLoader._loadScript({ url });
99
+ } catch {
100
+ status = 'error';
101
+ }
102
+ rtlPluginLoader.fire(new Event('RTLPluginLoaded'));
59
103
  }
60
- }
61
104
 
62
- function registerRTLTextPlugin(loadedPlugin) {
63
- if (plugin.isLoaded()) {
64
- throw new Error('RTL text plugin already registered.');
105
+ /** Start a lazy loading process of RTL plugin */
106
+ function lazyLoad() {
107
+ if (status === 'unavailable') {
108
+ status = 'requested';
109
+ return;
110
+ }
111
+ if (status === 'deferred') {
112
+ return _downloadRTLTextPlugin();
113
+ }
65
114
  }
66
- plugin['applyArabicShaping'] = loadedPlugin.applyArabicShaping;
67
- plugin['processBidirectionalText'] = loadedPlugin.processBidirectionalText;
68
- plugin['processStyledBidirectionalText'] = loadedPlugin.processStyledBidirectionalText;
69
115
 
70
- if (_completionCallback) {
71
- _completionCallback();
116
+ function _setMethods(rtlTextPlugin) {
117
+ if (!rtlTextPlugin) {
118
+ // for testing only
119
+ rtlPlugin.processStyledBidirectionalText = null;
120
+ rtlPlugin.processBidirectionalText = null;
121
+ rtlPlugin.applyArabicShaping = null;
122
+ return;
123
+ }
124
+ rtlPlugin.applyArabicShaping = rtlTextPlugin.applyArabicShaping;
125
+ rtlPlugin.processBidirectionalText = rtlTextPlugin.processBidirectionalText;
126
+ rtlPlugin.processStyledBidirectionalText = rtlTextPlugin.processStyledBidirectionalText;
72
127
  }
73
- _loadedCallbacks.forEach(callback => callback());
74
- _loadedCallbacks.length = 0;
75
- }
76
128
 
77
- globalThis.registerRTLTextPlugin ??= registerRTLTextPlugin;
129
+ // This is invoked by the RTL text plugin when the download has finished, and the code has been parsed.
130
+ function _registerRTLTextPlugin(rtlTextPlugin) {
131
+ if (rtlPlugin.isRTLSupported()) {
132
+ throw new Error('RTL text plugin already registered.');
133
+ }
134
+ status = 'loaded';
135
+ _setMethods(rtlTextPlugin);
136
+ }
137
+
138
+ return self;
139
+ }
78
140
 
79
- function loadScript(url) {
141
+ // public for testing
142
+ function _loadScript({ url }) {
80
143
  const { promise, resolve, reject } = Promise.withResolvers();
81
144
  const s = dynload(url);
82
145
  s.onload = () => resolve();
@@ -84,11 +147,15 @@ function loadScript(url) {
84
147
  return promise;
85
148
  }
86
149
 
87
- export const plugin = (rtlPlugin.plugin = {
88
- applyArabicShaping: null,
89
- processBidirectionalText: null,
90
- processStyledBidirectionalText: null,
91
- isLoaded: () => plugin.applyArabicShaping != null
92
- });
150
+ const { getRTLTextPluginStatus, setRTLTextPlugin, lazyLoad, _clearRTLTextPlugin, _registerRTLTextPlugin } =
151
+ RTLPluginLoader();
152
+
153
+ globalThis.registerRTLTextPlugin ??= _registerRTLTextPlugin;
93
154
 
94
- export default rtlPlugin;
155
+ export const rtlPluginLoader = Object.assign(new Evented(), {
156
+ getRTLTextPluginStatus,
157
+ setRTLTextPlugin,
158
+ lazyLoad,
159
+ _clearRTLTextPlugin,
160
+ _loadScript
161
+ });
@@ -148,6 +148,8 @@ async function finalizeBuckets(params, options, resources) {
148
148
  }
149
149
 
150
150
  async function makeAtlasses({ glyphDependencies, patternDependencies, iconDependencies }, resources) {
151
+ // options.glyphDependencies looks like: {"SomeFontName":{"10":true,"32":true}}
152
+ // this line makes an object like: {"SomeFontName":[10,32]}
151
153
  const stacks = mapObject(glyphDependencies, glyphs => Object.keys(glyphs).map(Number));
152
154
  const icons = Object.keys(iconDependencies);
153
155
  const patterns = Object.keys(patternDependencies);
@@ -1,4 +1,4 @@
1
- import { plugin as rtlTextPlugin } from '../source/rtl_text_plugin.js';
1
+ import { rtlPlugin } from '../source/rtl_text_plugin.js';
2
2
  import { isStringInSupportedScript } from '../util/script_detection.js';
3
3
  import ZoomHistory from './zoom_history.js';
4
4
 
@@ -20,7 +20,6 @@ export default class EvaluationParameters {
20
20
  this.transition = {};
21
21
  }
22
22
  }
23
-
24
23
  crossFadingFactor() {
25
24
  if (this.fadeDuration === 0) {
26
25
  return 1;
@@ -40,5 +39,5 @@ export default class EvaluationParameters {
40
39
  }
41
40
 
42
41
  function isSupportedScript(str) {
43
- return isStringInSupportedScript(str, rtlTextPlugin.isLoaded());
42
+ return isStringInSupportedScript(str, rtlPlugin.isRTLSupported());
44
43
  }
@@ -5,7 +5,7 @@ import ImageManager from '../render/image_manager.js';
5
5
  import LineAtlas from '../render/line_atlas.js';
6
6
  import { queryRenderedFeatures, queryRenderedSymbols, querySourceFeatures } from '../source/query_features.js';
7
7
  import { resources } from '../source/resources/index.js';
8
- import plugin from '../source/rtl_text_plugin.js';
8
+ import { rtlPluginLoader } from '../source/rtl_text_plugin.js';
9
9
  import { getType as getSourceType, setType as setSourceType } from '../source/source.js';
10
10
  import SourceCache from '../source/source_cache.js';
11
11
  import CrossTileSymbolIndex from '../symbol/cross_tile_symbol_index.js';
@@ -42,6 +42,7 @@ class Style extends Evented {
42
42
  });
43
43
  #layerIndex = new StyleLayerIndex();
44
44
  #opsQueue = [];
45
+ #rtlPluginLoadedHandler;
45
46
 
46
47
  constructor(map, options = {}) {
47
48
  super();
@@ -63,9 +64,8 @@ class Style extends Evented {
63
64
  this._updatedLayers = new Map();
64
65
  this._removedLayers = new Map();
65
66
  this._resetUpdates();
66
-
67
- this._rtlTextPluginCallbackUnregister = plugin.registerForPluginAvailability(this._reloadSources.bind(this));
68
-
67
+ this.#rtlPluginLoadedHandler = this.#rtlPluginLoaded.bind(this);
68
+ rtlPluginLoader.on('RTLPluginLoaded', this.#rtlPluginLoadedHandler);
69
69
  this.on('data', event => {
70
70
  if (event.dataType !== 'source' || event.sourceDataType !== 'metadata') {
71
71
  return;
@@ -89,6 +89,18 @@ class Style extends Evented {
89
89
  });
90
90
  }
91
91
 
92
+ #rtlPluginLoaded() {
93
+ for (const sourceCache of Object.values(this._sources)) {
94
+ const { type } = sourceCache.getSource();
95
+ if (type === 'vector' || type === 'geojson') {
96
+ // Non-vector sources don't have any symbols buckets to reload when the RTL text plugin loads
97
+ // They also load more quickly, so they're more likely to have already displaying tiles
98
+ // that would be unnecessarily booted by the plugin load event
99
+ sourceCache.reload(); // Should be a no-op if the plugin loads before any tiles load
100
+ }
101
+ }
102
+ }
103
+
92
104
  setGlobalStateProperty(name, value) {
93
105
  if (!this._loaded) {
94
106
  this.#opsQueue.push(() => this.setGlobalStateProperty(name, value));
@@ -1043,7 +1055,7 @@ class Style extends Evented {
1043
1055
  }
1044
1056
 
1045
1057
  _remove() {
1046
- this._rtlTextPluginCallbackUnregister?.();
1058
+ rtlPluginLoader.off('RTLPluginLoaded', this.#rtlPluginLoadedHandler);
1047
1059
  for (const id in this._sources) {
1048
1060
  this._sources[id].clearTiles();
1049
1061
  }
@@ -1064,12 +1076,6 @@ class Style extends Evented {
1064
1076
  }
1065
1077
  }
1066
1078
 
1067
- _reloadSources() {
1068
- for (const sourceCache of Object.values(this._sources)) {
1069
- sourceCache.reload(); // Should be a no-op if called before any tiles load
1070
- }
1071
- }
1072
-
1073
1079
  _generateCollisionBoxes() {
1074
1080
  for (const id in this._sources) {
1075
1081
  this._reloadSource(id);
@@ -1,4 +1,4 @@
1
- import { plugin as rtlTextPlugin } from '../source/rtl_text_plugin.js';
1
+ import { rtlPlugin } from '../source/rtl_text_plugin.js';
2
2
  import { charAllowsIdeographicBreaking, charHasUprightVerticalOrientation } from '../util/script_detection.js';
3
3
  import verticalizePunctuation from '../util/verticalize_punctuation.js';
4
4
  import ONE_EM from './one_em.js';
@@ -112,7 +112,7 @@ export function shapeText(
112
112
 
113
113
  let lines;
114
114
 
115
- const { processBidirectionalText, processStyledBidirectionalText } = rtlTextPlugin;
115
+ const { processBidirectionalText, processStyledBidirectionalText } = rtlPlugin;
116
116
  if (processBidirectionalText && logicalInput.sections.length === 1) {
117
117
  // Bidi doesn't have to be style-aware
118
118
  lines = [];
@@ -1,4 +1,4 @@
1
- import { plugin as rtlTextPlugin } from '../source/rtl_text_plugin.js';
1
+ import { rtlPlugin } from '../source/rtl_text_plugin.js';
2
2
 
3
3
  function transformText(text, layer, feature) {
4
4
  const transform = layer._layout.get('text-transform').evaluate(feature, {});
@@ -7,9 +7,8 @@ function transformText(text, layer, feature) {
7
7
  } else if (transform === 'lowercase') {
8
8
  text = text.toLocaleLowerCase();
9
9
  }
10
-
11
- if (rtlTextPlugin.applyArabicShaping) {
12
- text = rtlTextPlugin.applyArabicShaping(text);
10
+ if (rtlPlugin.applyArabicShaping) {
11
+ text = rtlPlugin.applyArabicShaping(text);
13
12
  }
14
13
 
15
14
  return text;
@@ -1,9 +1,4 @@
1
1
  import { Evented } from '@mapwhit/events';
2
- import browser from './browser.js';
3
-
4
- function getDefaultWorkerCount() {
5
- return Math.max(Math.floor(browser.hardwareConcurrency / 2), 1);
6
- }
7
2
 
8
3
  const config = new Evented();
9
4
 
@@ -18,7 +13,4 @@ config.notify = function () {
18
13
  config.fire('change', config);
19
14
  };
20
15
 
21
- config.set({
22
- WORKER_COUNT: getDefaultWorkerCount(),
23
- WORKER_URL: ''
24
- });
16
+ config.set({});
@@ -415,6 +415,15 @@ export function charHasRotatedVerticalOrientation(char) {
415
415
  return !(charHasUprightVerticalOrientation(char) || charHasNeutralVerticalOrientation(char));
416
416
  }
417
417
 
418
+ export function charInRTLScript(char) {
419
+ // Main blocks for Hebrew, Arabic, Thaana and other RTL scripts
420
+ return (
421
+ (char >= 0x0590 && char <= 0x08ff) ||
422
+ isChar['Arabic Presentation Forms-A'](char) ||
423
+ isChar['Arabic Presentation Forms-B'](char)
424
+ );
425
+ }
426
+
418
427
  export function charInSupportedScript(char, canRenderRTL) {
419
428
  // This is a rough heuristic: whether we "can render" a script
420
429
  // actually depends on the properties of the font being used
@@ -423,13 +432,7 @@ export function charInSupportedScript(char, canRenderRTL) {
423
432
 
424
433
  // Even in Latin script, we "can't render" combinations such as the fi
425
434
  // ligature, but we don't consider that semantically significant.
426
- if (
427
- !canRenderRTL &&
428
- ((char >= 0x0590 && char <= 0x08ff) ||
429
- isChar['Arabic Presentation Forms-A'](char) ||
430
- isChar['Arabic Presentation Forms-B'](char))
431
- ) {
432
- // Main blocks for Hebrew, Arabic, Thaana and other RTL scripts
435
+ if (!canRenderRTL && charInRTLScript(char)) {
433
436
  return false;
434
437
  }
435
438
  if (
@@ -448,6 +451,15 @@ export function charInSupportedScript(char, canRenderRTL) {
448
451
  return true;
449
452
  }
450
453
 
454
+ export function stringContainsRTLText(chars) {
455
+ for (const char of chars) {
456
+ if (charInRTLScript(char.charCodeAt(0))) {
457
+ return true;
458
+ }
459
+ }
460
+ return false;
461
+ }
462
+
451
463
  export function isStringInSupportedScript(chars, canRenderRTL) {
452
464
  for (const char of chars) {
453
465
  if (!charInSupportedScript(char.charCodeAt(0), canRenderRTL)) {