@mapwhit/tilerenderer 1.4.0 → 1.6.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.6.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.6.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
@@ -25,7 +25,6 @@
25
25
  "@mapwhit/vector-tile": "4.0.0",
26
26
  "@pirxpilot/nanoassert": "~1",
27
27
  "csscolorparser": "^1.0.3",
28
- "dynload": "^1.0.2",
29
28
  "earcut": "^3.0.1",
30
29
  "geojson-vt": "^4.0.2",
31
30
  "grid-index": "^1.1.0",
@@ -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);
@@ -59,16 +43,37 @@ export default mapwhit;
59
43
  */
60
44
 
61
45
  /**
62
- * Sets the map's [RTL text plugin](https://www.mapbox.com/mapbox-gl-js/plugins/#mapbox-gl-rtl-text).
46
+ * Sets the map's [RTL text plugin](https://github.com/mapwhit/rtl-text).
63
47
  * Necessary for supporting languages like Arabic and Hebrew that are written right-to-left.
64
48
  *
65
49
  * @function setRTLTextPlugin
66
- * @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.
50
+ * @param {function} loadPlugin a function that returns a Promise resolving to object
51
+ * with RTL text plugin methods `applyArabicShaping`, `processBidirectionalText`,
52
+ * and `processStyledBidirectionalText`.
53
+ * @param {boolean} lazy If set to `true`, loading the plugin will defer until rtl text is encountered,
54
+ * rtl text will then be rendered only after the plugin finishes loading.
55
+ * @example
56
+ * ```javascript
57
+ * import loadRTLTextPlugin from '@mapwhit/rtl-text';
58
+ * setRTLTextPlugin(loadRTLTextPlugin, true);
59
+ * ```
60
+ * @see [Add support for right-to-left scripts](https://maplibre.org/maplibre-gl-js/docs/examples/mapbox-gl-rtl-text/)
61
+ */
62
+ function setRTLTextPlugin(loadPlugin, lazy) {
63
+ return rtlPluginLoader.setRTLTextPlugin(loadPlugin, lazy);
64
+ }
65
+ /**
66
+ * Gets the map's [RTL text plugin](https://github.com/mapwhit/rtl-text) status.
67
+ * The status can be `unavailable` (i.e. not requested or removed), `loading`, `loaded` or `error`.
68
+ * If the status is `loaded` and the plugin is requested again, an error will be thrown.
69
+ *
70
+ * @function getRTLTextPluginStatus
68
71
  * @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/)
72
+ * const pluginStatus = getRTLTextPluginStatus();
71
73
  */
74
+ function getRTLTextPluginStatus() {
75
+ return rtlPluginLoader.getRTLTextPluginStatus();
76
+ }
72
77
 
73
78
  // canary assert: used to confirm that asserts have been removed from production build
74
79
  import assert from 'assert';
@@ -1,94 +1,130 @@
1
- import dynload from 'dynload';
2
- import browser from '../util/browser.js';
1
+ import { Event, Evented } from '@mapwhit/events';
3
2
 
4
- let pluginRequested = false;
5
- let pluginURL;
6
- let loading = false;
3
+ /**
4
+ * The possible option of the plugin's status
5
+ *
6
+ * `unavailable`: Not loaded.
7
+ *
8
+ * `deferred`: The plugin URL has been specified, but loading has been deferred.
9
+ *
10
+ * `loading`: request in-flight.
11
+ *
12
+ * `loaded`: The plugin is now loaded
13
+ *
14
+ * `error`: The plugin failed to load
15
+ */
7
16
 
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
- };
17
+ function RTLPlugin() {
18
+ const self = {
19
+ isRTLSupported
20
+ };
17
21
 
18
- export function registerForPluginAvailability(callback) {
19
- if (plugin.isLoaded()) {
20
- callback();
21
- return;
22
+ /**
23
+ * Checks whether the RTL language support is available.
24
+ * If `canChangeShortly` is false, it will only return true if the RTL language
25
+ * is properly supported.
26
+ * If `canChangeShortly` is true, it will also return true if the RTL language
27
+ * is not supported unless it can obtain the RTL text plugin.
28
+ * @param {boolean} canChangeShortly
29
+ * @returns
30
+ */
31
+ function isRTLSupported(canChangeShortly) {
32
+ if (rtlPluginLoader.getRTLTextPluginStatus() === 'loaded') {
33
+ return true;
34
+ }
35
+ if (!canChangeShortly) {
36
+ return false;
37
+ }
38
+ rtlPluginLoader.lazyLoad();
39
+ // any transitive state other than 'loading' means we can consider RTL supported as best as possible for now
40
+ return rtlPluginLoader.getRTLTextPluginStatus() !== 'loading';
22
41
  }
23
- _loadedCallbacks.push(callback);
24
- loadRTLTextPlugin();
25
- return () => _loadedCallbacks.splice(_loadedCallbacks.indexOf(callback), 1);
26
- }
27
42
 
28
- export function clearRTLTextPlugin() {
29
- _loadedCallbacks.length = 0;
30
- pluginRequested = false;
31
- pluginURL = undefined;
43
+ return self;
32
44
  }
33
45
 
34
- export function setRTLTextPlugin(url, callback) {
35
- if (pluginRequested) {
36
- throw new Error('setRTLTextPlugin cannot be called multiple times.');
46
+ export const rtlPlugin = RTLPlugin();
47
+
48
+ function RTLPluginLoader() {
49
+ let status = 'unavailable';
50
+ let load;
51
+
52
+ const self = {
53
+ getRTLTextPluginStatus,
54
+ setRTLTextPlugin,
55
+ lazyLoad,
56
+ _clearRTLTextPlugin
57
+ };
58
+
59
+ /** This one is exposed to outside */
60
+ function getRTLTextPluginStatus() {
61
+ return status;
62
+ }
63
+
64
+ // public for testing
65
+ function _clearRTLTextPlugin() {
66
+ status = 'unavailable';
67
+ load = undefined;
68
+ _setMethods();
37
69
  }
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));
70
+
71
+ function setRTLTextPlugin(pluginLoad, deferred = false) {
72
+ if (load) {
73
+ // error
74
+ return Promise.reject(new Error('setRTLTextPlugin cannot be called multiple times.'));
75
+ }
76
+ load = pluginLoad;
77
+ if (status === 'requested') {
78
+ return _downloadRTLTextPlugin();
79
+ }
80
+ if (status === 'unavailable') {
81
+ // from initial state:
82
+ if (!deferred) {
83
+ return _downloadRTLTextPlugin();
47
84
  }
85
+ status = 'deferred';
48
86
  }
49
- loading = false;
50
- _completionCallback = undefined;
51
- };
52
- loadRTLTextPlugin();
53
- }
54
-
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);
59
87
  }
60
- }
61
88
 
62
- function registerRTLTextPlugin(loadedPlugin) {
63
- if (plugin.isLoaded()) {
64
- throw new Error('RTL text plugin already registered.');
89
+ async function _downloadRTLTextPlugin() {
90
+ if (typeof load !== 'function') {
91
+ return Promise.reject(new Error('RTL text plugin load function is not set.'));
92
+ }
93
+ status = 'loading';
94
+ try {
95
+ _setMethods(await load());
96
+ status = 'loaded';
97
+ } catch {
98
+ status = 'error';
99
+ }
100
+ rtlPluginLoader.fire(new Event('RTLPluginLoaded'));
65
101
  }
66
- plugin['applyArabicShaping'] = loadedPlugin.applyArabicShaping;
67
- plugin['processBidirectionalText'] = loadedPlugin.processBidirectionalText;
68
- plugin['processStyledBidirectionalText'] = loadedPlugin.processStyledBidirectionalText;
69
102
 
70
- if (_completionCallback) {
71
- _completionCallback();
103
+ /** Start a lazy loading process of RTL plugin */
104
+ function lazyLoad() {
105
+ if (status === 'unavailable') {
106
+ status = 'requested';
107
+ return;
108
+ }
109
+ if (status === 'deferred') {
110
+ return _downloadRTLTextPlugin();
111
+ }
72
112
  }
73
- _loadedCallbacks.forEach(callback => callback());
74
- _loadedCallbacks.length = 0;
75
- }
76
113
 
77
- globalThis.registerRTLTextPlugin ??= registerRTLTextPlugin;
114
+ function _setMethods(rtlTextPlugin) {
115
+ if (!rtlTextPlugin) {
116
+ // for testing only
117
+ rtlPlugin.processStyledBidirectionalText = null;
118
+ rtlPlugin.processBidirectionalText = null;
119
+ rtlPlugin.applyArabicShaping = null;
120
+ return;
121
+ }
122
+ rtlPlugin.applyArabicShaping = rtlTextPlugin.applyArabicShaping;
123
+ rtlPlugin.processBidirectionalText = rtlTextPlugin.processBidirectionalText;
124
+ rtlPlugin.processStyledBidirectionalText = rtlTextPlugin.processStyledBidirectionalText;
125
+ }
78
126
 
79
- function loadScript(url) {
80
- const { promise, resolve, reject } = Promise.withResolvers();
81
- const s = dynload(url);
82
- s.onload = () => resolve();
83
- s.onerror = () => reject(true);
84
- return promise;
127
+ return self;
85
128
  }
86
129
 
87
- export const plugin = (rtlPlugin.plugin = {
88
- applyArabicShaping: null,
89
- processBidirectionalText: null,
90
- processStyledBidirectionalText: null,
91
- isLoaded: () => plugin.applyArabicShaping != null
92
- });
93
-
94
- export default rtlPlugin;
130
+ export const rtlPluginLoader = Object.assign(new Evented(), RTLPluginLoader());
@@ -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)) {