library/maps/rtl-text.js installed the RTL text plugin for MAPBOX only (window.mapboxgl, from api.mapbox.com). So every MapLibre map, i.e. every keyless embed, drew RTL place names as disconnected, reversed glyphs. Measured on the Prayer Walk Planner's printed Arabic sheet. Under it was a second gap. Bundle entries call the installer at LOAD time, before useMapInstance has lazily injected a MapLibre engine, so even an engine-aware installer found nothing to install into. - rtl-text.js is engine-aware. Mapbox is unchanged (same URL, same lazy call). MapLibre gets the same plugin package from jsDelivr, keyless and not api.mapbox.com, so keyless pages still make zero Mapbox requests. New ensureRtlTextPlugin(gl) waits until it is LOADED, for callers that capture a single frame (print). - useMapInstance installs it the moment an engine global exists, the one point every map passes through. - The planner drops its private copy for the shared helper. Measured: a print map of downtown Cairo (MapLibre, keyless) shows shaped Arabic street and place names (شارع التحرير, مسجد الطباخ …); plugin status "loaded", fetched from jsDelivr. check:gallery-embed passes js, iframe, neighbour and linked (14 tabs); :maplibre passes; check:prayer-walk-planner passes. The shared change reaches all 11 bundles that compile initializeMap, and all are rebuilt here. check:shared-reached gains an rtl-text-maplibre marker (10/10 consumers carry it; cities-internet, gospel-resources and my-upg-100-list boot maps another way and are not affected). Also: the new Experimental bundle pplr-data-maps loads the Mapbox geocoder under maplibre (pplrData.js:38). It is an owned, printed MAPBOX_KNOWN entry routed to its owner; delete it in their fix commit.
97 lines
5.6 KiB
JavaScript
97 lines
5.6 KiB
JavaScript
/**
|
|
* rtl-text.js — load the RTL text plugin ONCE per page, for Mapbox AND MapLibre.
|
|
*
|
|
* WHY THIS IS A SHARED FUNCTION AND NOT A COPY-PASTE BLOCK. Every bundle entry that boots a map
|
|
* needs this, and the block was duplicated into each `index.js` by hand. A baked cap-4 composite
|
|
* imports its child's `.vue` directly and therefore never runs the child's entry, so it has to
|
|
* re-declare everything the entry does — and `dashboard-plus-grid` re-declared Pinia, i18n and the
|
|
* colour strategies but NOT this one.
|
|
*
|
|
* MEASURED in the shipped bundles 2026-09-22:
|
|
* research-dashboard.js ....... 1 occurrence of mapbox-gl-rtl-text
|
|
* doxa-research-map.js ........ 1
|
|
* dashboard-plus-grid.js ...... 0 <- the composite
|
|
* So Arabic, Hebrew and Persian labels render REVERSED inside the cap-5 shell and correctly
|
|
* standalone. That is not cosmetic for this product: Arab World (99 rows) and Persian-Median
|
|
* (49 rows) are in scope, and a reversed label is a WRONG NAME for exactly the groups the map is
|
|
* about. It also fails silently — no error, no console warning, just mirrored text that looks like
|
|
* a font problem to anyone who does not read the script.
|
|
*
|
|
* ★ THE GENERAL RULE THIS EXISTS TO SERVE: a composite that BAKES a child's `.vue` inherits nothing
|
|
* from that child's entry file; a composite that COMPOSES by tag + bundle URL boots the child
|
|
* through its own entry and inherits everything. `stack-demo` and `research-map-plus-grid` compose,
|
|
* which is why they were never affected. Only `dashboard-plus-grid` bakes.
|
|
*
|
|
* Four of the five entry side effects fail SILENTLY (Pinia, i18n, colour strategies, this one); only
|
|
* `provide('profileModules')` throws, and it is the one that is safe to omit — structurally, because
|
|
* a baked composite does not use ProfileLoader.vue. So "it renders" is no evidence that an entry's
|
|
* work was done.
|
|
*
|
|
* Idempotent by construction: Mapbox throws if the plugin is set twice in a session, and that throw
|
|
* is swallowed here, so calling this from several entries on one page is safe.
|
|
*/
|
|
|
|
// ONE PLUGIN, TWO ENGINES, TWO SOURCES. Mapbox keeps its own CDN (unchanged). MapLibre gets the same
|
|
// plugin package from jsDelivr — keyless and NOT api.mapbox.com, so a keyless page (provider
|
|
// maplibre) still makes zero Mapbox requests (check:gallery-embed:maplibre enforces that).
|
|
export const RTL_PLUGIN_URLS = {
|
|
mapbox: 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.3.0/mapbox-gl-rtl-text.js',
|
|
maplibre: 'https://cdn.jsdelivr.net/npm/@mapbox/mapbox-gl-rtl-text@0.3.0/dist/mapbox-gl-rtl-text.js'
|
|
}
|
|
const RTL_PLUGIN_URL = RTL_PLUGIN_URLS.mapbox // kept for readers grepping the old name
|
|
|
|
// ★ MAPLIBRE WAS NEVER COVERED (2026-09-25). This used to check window.mapboxgl only, so every
|
|
// MapLibre map — every keyless embed — drew Arabic, Hebrew and Persian labels as disconnected,
|
|
// reversed glyphs. Measured on the Prayer Walk Planner's printed Arabic sheet. A second gap sat under
|
|
// it: bundle entries call this at LOAD time, before useMapInstance has injected a MapLibre engine,
|
|
// so even an engine-aware version found nothing to install into. useMapInstance now also calls it
|
|
// the moment an engine global exists, which is what makes it reach every map.
|
|
function isMaplibre(gl) { return !!gl && typeof window !== 'undefined' && gl === window.maplibregl }
|
|
|
|
/**
|
|
* Install the RTL text plugin, lazily, on the given engine — or on whichever engine globals exist.
|
|
* Idempotent: safe from several entries and from every map init on one page.
|
|
* @param {object} [engine] window.mapboxgl or window.maplibregl; omitted = both, if present
|
|
* @returns {boolean} true when this call installed it
|
|
*/
|
|
export function installRtlTextPlugin(engine) {
|
|
if (typeof window === 'undefined') return false
|
|
const engines = engine ? [engine] : [window.maplibregl, window.mapboxgl].filter(Boolean)
|
|
let installed = false
|
|
for (const gl of engines) {
|
|
if (!gl || !gl.setRTLTextPlugin) continue
|
|
try {
|
|
if (isMaplibre(gl)) {
|
|
// MapLibre: (url, lazy) → Promise. Guard on status: a second call rejects.
|
|
if (gl.getRTLTextPluginStatus && gl.getRTLTextPluginStatus() !== 'unavailable') continue
|
|
const r = gl.setRTLTextPlugin(RTL_PLUGIN_URLS.maplibre, true)
|
|
if (r && r.catch) r.catch(() => {})
|
|
} else {
|
|
// `true` = LAZY: fetch the plugin only when RTL text is actually encountered, so a map
|
|
// with no RTL labels pays nothing.
|
|
gl.setRTLTextPlugin(RTL_PLUGIN_URLS.mapbox, null, true)
|
|
}
|
|
installed = true
|
|
} catch (_) {
|
|
// Already set in this session. Expected whenever two entries run on one page — not an error.
|
|
}
|
|
}
|
|
return installed
|
|
}
|
|
|
|
/**
|
|
* Make sure the plugin is LOADED (not merely registered) before a single frame is captured — a
|
|
* print or snapshot takes one frame, and a lazily-loaded plugin can still be in flight when it
|
|
* does. MapLibre only (Mapbox has no status API); resolves after at most ~5s either way.
|
|
*/
|
|
export async function ensureRtlTextPlugin(gl) {
|
|
if (!gl || !isMaplibre(gl) || !gl.getRTLTextPluginStatus) return
|
|
try {
|
|
// Not registered yet → register EAGERLY. Already registered lazily ('deferred') → it loads on the
|
|
// first RTL label this map draws, so just wait for it below.
|
|
if (gl.getRTLTextPluginStatus() === 'unavailable') await gl.setRTLTextPlugin(RTL_PLUGIN_URLS.maplibre, false)
|
|
for (let i = 0; i < 50 && !['loaded', 'error'].includes(gl.getRTLTextPluginStatus()); i++) await new Promise(r => setTimeout(r, 100))
|
|
} catch (_) { /* shaping is a nicety; the map still renders */ }
|
|
}
|
|
|
|
export default installRtlTextPlugin
|