Raw maps shape Arabic, Persian and Hebrew place names: cities-internet and key-cities
These two maps build `new gl.Map` directly, so they never got useMapInstance's RTL install, and their bundle entries registered nothing either. The basemap draws each place in its OWN script, so over the Middle East most labels are Arabic, Persian or Hebrew for EVERY viewer: they were drawn as disconnected glyphs in the wrong order. installRtlTextPlugin(gl) now runs after the engine global resolves and before new gl.Map (an entry-time call cannot work once the engine is injected lazily). Gate: check:rtl-raw (+ :poison), gospel's check-gospel-rtl-shaping method (pan to Arabia first; no RTL label on screen is NOT MEASURED; one defineProperty per global for hook + poison) over cities-internet on both engines and key-cities (Mapbox-only, gated, no leaf: mounted on a same-origin host page). The shared gate's --leaf argument can replace the cities rows once it lands; the key-cities host page still needs this one. Measured: before, cities-internet mapbox 58 RTL labels with the shaper 'unavailable' and maplibre 62 stuck at 'requested', both unshaped; after, shaper 'loaded' on both, and key-cities 56 RTL labels shaped. Poison: 3/3 rows caught.
This commit is contained in:
parent
1828957f83
commit
ffef2b761e
4 changed files with 167 additions and 0 deletions
155
internal/check-raw-map-rtl-shaping.mjs
Normal file
155
internal/check-raw-map-rtl-shaping.mjs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-raw-map-rtl-shaping — the RAW maps (built with `new gl.Map`, not useMapInstance) must
|
||||
* shape Arabic, Hebrew and Persian place names.
|
||||
*
|
||||
* gospel's check-gospel-rtl-shaping.mjs (b9dd5545) established the method on resources-map; this
|
||||
* is the same gate for the other raw maps: cities-internet/connectivity-map (both engines) and
|
||||
* key-cities/cities-map (Mapbox-only; gated, so it has no leaf page and is mounted on a
|
||||
* same-origin host page). Read that file's header for the full reasoning; the load-bearing parts:
|
||||
*
|
||||
* - WHY IT MATTERS TO EVERYONE: these maps do not localise basemap labels, so each place is drawn
|
||||
* in its OWN script. Over the Middle East most labels are Arabic/Persian/Hebrew for EVERY
|
||||
* viewer in every UI language. Unshaped = disconnected glyphs in the wrong order.
|
||||
* - IT PANS FIRST: the shaper registers lazily and reads 'deferred' at a world view. Only after
|
||||
* RTL labels are really on screen does 'loaded' mean they went through the shaper. No RTL label
|
||||
* on screen is NOT MEASURED, never PASS.
|
||||
* - ONE property definition per engine global does both the map hook and the poison: two
|
||||
* addInitScript defineProperty calls on one global silently replace each other.
|
||||
*
|
||||
* WHY THE FIX HAS TO BE IN THE MAP, NOT THE ENTRY: bundle entries used to call setRTLTextPlugin at
|
||||
* load time, which only worked because every leaf <head> preloaded mapbox-gl.js. The engine is now
|
||||
* injected lazily (ensureEngineScript), after the entry has run, so the call has to follow the
|
||||
* engine global and precede `new gl.Map`: installRtlTextPlugin(gl) from library/maps/rtl-text.js.
|
||||
*
|
||||
* --poison neuters setRTLTextPlugin in the page (the real pre-fix state) and must be caught on
|
||||
* every measured row. Exit: 0 pass · 1 fail · 2 not measured.
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import { serveReaped } from './_serve-reaped.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const PORT = Number(process.env.RTL_RAW_PORT || 5394)
|
||||
const POISON = process.argv.includes('--poison')
|
||||
const TK = process.env.MAPBOX_TOKEN || JSON.parse(readFileSync(resolve(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8')).token || ''
|
||||
const BASE = `http://127.0.0.1:${PORT}/${TREE_NAME}`
|
||||
/* Arabia: the densest concentration of rendered Arabic on the basemap. */
|
||||
const RTL_VIEW = { center: [39.2, 24.5], zoom: 5 }
|
||||
|
||||
const HOST_PATH = `${TREE_NAME}/key-cities/__rtl-host.html`
|
||||
const TARGETS = [
|
||||
...['mapbox', 'maplibre'].map((provider) => ({
|
||||
name: 'cities-internet', provider,
|
||||
url: `${BASE}/cities-internet/connectivity-map/index.html?provider=${provider}&lang=ar` + (provider === 'mapbox' ? `&tk=${TK}` : ''),
|
||||
})),
|
||||
{ name: 'key-cities', provider: 'mapbox', url: `http://127.0.0.1:${PORT}/${HOST_PATH}`, host: true },
|
||||
]
|
||||
const HOST_HTML = `<!DOCTYPE html><html lang="ar"><head><meta charset="utf-8"></head><body style="margin:0">
|
||||
<script src="/${TREE_NAME}/key-cities/key-cities.js"></script>
|
||||
<key-cities-map profile-config='{"profile":"cities-map","tk":"${TK}","lang":"ar"}' style="display:block;height:800px"></key-cities-map>
|
||||
</body></html>`
|
||||
|
||||
const srv = serveReaped(resolve(ROOT, 'public'), PORT)
|
||||
await new Promise((r) => setTimeout(r, 2500))
|
||||
const browser = await chromium.launch({
|
||||
args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'],
|
||||
})
|
||||
|
||||
const INSTALL = (poison) => {
|
||||
const define = (name) => {
|
||||
let v
|
||||
Object.defineProperty(window, name, {
|
||||
configurable: true,
|
||||
get: () => v,
|
||||
set: (g) => {
|
||||
v = g
|
||||
if (!g) return
|
||||
if (poison && g.setRTLTextPlugin) g.setRTLTextPlugin = () => {}
|
||||
if (g.Map && !g.__hooked) {
|
||||
g.__hooked = true
|
||||
const Orig = g.Map
|
||||
g.Map = function (...a) { const m = new Orig(...a); window.__MAP = m; return m }
|
||||
g.Map.prototype = Orig.prototype
|
||||
Object.setPrototypeOf(g.Map, Orig)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
define('mapboxgl'); define('maplibregl')
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const t of TARGETS) {
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } })
|
||||
await page.addInitScript(INSTALL, POISON)
|
||||
if (t.host) await page.route(/__rtl-host\.html/, (r) => r.fulfill({ contentType: 'text/html', body: HOST_HTML }))
|
||||
let r
|
||||
try {
|
||||
await page.goto(t.url, { waitUntil: 'networkidle', timeout: 90000 })
|
||||
await page.waitForTimeout(8000)
|
||||
r = await page.evaluate(async (view) => {
|
||||
const m = window.__MAP
|
||||
if (!m) return { err: 'the page never constructed a map' }
|
||||
const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
|
||||
// Wait for the style before moving: an unloaded style answers ZERO features.
|
||||
for (let i = 0; i < 60 && !m.isStyleLoaded(); i++) await sleep(500)
|
||||
m.jumpTo(view)
|
||||
await new Promise((res) => { m.once('idle', res); setTimeout(res, 15000) })
|
||||
let names = []
|
||||
for (let i = 0; i < 30; i++) {
|
||||
names = m.queryRenderedFeatures()
|
||||
.filter((f) => f.layer && f.layer.type === 'symbol')
|
||||
.map((f) => String((f.properties && (f.properties.name || f.properties.name_en)) || ''))
|
||||
.filter(Boolean)
|
||||
if (names.some((n) => /[-ۿݐ-ݿ-]/.test(n))) break
|
||||
await sleep(1000)
|
||||
}
|
||||
// the engine that built THIS map, not whichever global happens to exist
|
||||
const g = (window.maplibregl && m instanceof window.maplibregl.Map) ? window.maplibregl : window.mapboxgl
|
||||
return {
|
||||
rtl: names.filter((n) => /[-ۿݐ-ݿ-]/.test(n)),
|
||||
symbols: names.length,
|
||||
status: g && g.getRTLTextPluginStatus ? g.getRTLTextPluginStatus() : 'NO STATUS API',
|
||||
}
|
||||
}, RTL_VIEW)
|
||||
} catch (e) {
|
||||
r = { err: String(e.message).slice(0, 90) }
|
||||
}
|
||||
await page.close()
|
||||
results.push({ ...t, ...r })
|
||||
}
|
||||
await browser.close()
|
||||
srv.stop()
|
||||
|
||||
let bad = 0, unmeasured = 0
|
||||
for (const r of results) {
|
||||
const tag = `${r.name} ${r.provider}`
|
||||
if (r.err) { console.error(` ${tag}: NOT MEASURED — ${r.err}`); unmeasured++; continue }
|
||||
if (!r.rtl.length) {
|
||||
console.error(` ${tag}: NOT MEASURED — no RTL label rendered at the test view (${r.symbols} symbols); the gate cannot see the defect here.`)
|
||||
unmeasured++; continue
|
||||
}
|
||||
const sample = r.rtl.slice(0, 3).join(', ')
|
||||
if (r.status !== 'loaded') {
|
||||
console.error(` ${tag}: FAIL — ${r.rtl.length} RTL labels on screen (${sample}) but the shaper is '${r.status}'. These labels are drawn unshaped.`)
|
||||
bad++; continue
|
||||
}
|
||||
console.log(` ok ${tag} — ${r.rtl.length}/${r.symbols} rendered labels are RTL (${sample}), shaper loaded`)
|
||||
}
|
||||
const measured = results.length - unmeasured
|
||||
if (POISON) {
|
||||
if (unmeasured) { console.error(`RAW-RTL --poison: NOT MEASURED — ${unmeasured} row(s) had nothing to judge, so the self-test proves nothing for them.`); process.exitCode = 2 }
|
||||
else if (bad === measured) { console.log(`RAW-RTL --poison: ${bad}/${measured} rows caught with the plugin neutered.`); console.log('RAW-RTL: SELF-TEST PASS — the check fails as designed.') }
|
||||
else { console.error('RAW-RTL: SELF-TEST FAIL — a neutered shaper still passed.'); process.exitCode = 1 }
|
||||
} else if (bad) {
|
||||
console.error(`RAW-RTL: FAIL — ${bad} of ${results.length} row(s) draw RTL place names unshaped.`)
|
||||
console.error(' Fix: installRtlTextPlugin(gl) from library/maps/rtl-text.js, right after the engine global')
|
||||
console.error(' resolves and BEFORE new gl.Map.')
|
||||
process.exitCode = 1
|
||||
} else if (unmeasured) {
|
||||
console.error(`RAW-RTL: NOT MEASURED — ${unmeasured} of ${results.length} row(s) could not be judged.`); process.exitCode = 2
|
||||
} else console.log(`RAW-RTL: PASS — cities-internet (both engines) and key-cities shape RTL place names.`)
|
||||
|
|
@ -134,6 +134,8 @@
|
|||
"check:quoted-scripts": "node internal/check-quoted-scripts.mjs",
|
||||
"check:rtl-shaping": "node internal/check-gospel-rtl-shaping.mjs",
|
||||
"check:rtl-shaping:poison": "node internal/check-gospel-rtl-shaping.mjs --poison",
|
||||
"check:rtl-raw": "node internal/check-raw-map-rtl-shaping.mjs",
|
||||
"check:rtl-raw:poison": "node internal/check-raw-map-rtl-shaping.mjs --poison",
|
||||
"check:scrim-viewport": "node internal/check-scrim-covers-viewport.mjs",
|
||||
"check:scrim-viewport:self-test": "node internal/check-scrim-covers-viewport.mjs --self-test",
|
||||
"check:script-flags": "node internal/check-script-flags-reachable.mjs",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
|||
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
|
||||
import { ensureEngineScript, engineGlobal, ensureEngineCSS, engineStylesheetUrl } from '@map/composables/useMapInstance.js'
|
||||
import { resolveNamedStyle } from '@map/styles/registry.js'
|
||||
import { installRtlTextPlugin } from '@map/maps/rtl-text.js'
|
||||
// People-group dots open the SAME detail panel as Prayer Progress (doxa-simple-map): the
|
||||
// shared store selection, the shared GO-pin highlight, and the shared legend wrappers
|
||||
// that host PeopleGroupDetail on desktop and in the phone sheet.
|
||||
|
|
@ -278,6 +279,11 @@ onMounted(async () => {
|
|||
const gl = engineGlobal(ciProvider) || window.mapboxgl
|
||||
if (unmounted) return
|
||||
if (!gl || !mapEl.value) { err.value = 'map engine not loaded'; return }
|
||||
// RTL SHAPING, before the first map exists. This raw map does not go through useMapInstance
|
||||
// (which installs it), and the basemap draws each place in its OWN script: over the Middle East
|
||||
// most labels are Arabic, Persian or Hebrew for EVERY viewer. An entry-time call cannot do this,
|
||||
// because the engine is injected lazily, after the bundle has run.
|
||||
installRtlTextPlugin(gl)
|
||||
try {
|
||||
// The dots are a second layer, not a precondition: if the list fails, the map still shades.
|
||||
const groupsLoaded = loadGroups().catch((e) => { console.warn('[connectivity-map] people groups:', e?.message || e) })
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount
|
|||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { addMapAttribution } from '@map/composables/useMapAttribution'
|
||||
import { ensureEngineScript, engineGlobal } from '@map/composables/useMapInstance.js'
|
||||
import { installRtlTextPlugin } from '@map/maps/rtl-text.js'
|
||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||
|
||||
const profileConfig = inject('profileConfig', ref(null))
|
||||
|
|
@ -137,6 +138,9 @@ onMounted(async () => {
|
|||
try { await ensureEngineScript('mapbox') } catch (_) {}
|
||||
const gl = engineGlobal('mapbox')
|
||||
if (!gl || !mapEl.value) { err.value = 'mapbox-gl not loaded'; return }
|
||||
// RTL SHAPING before the map exists: a raw map is not covered by useMapInstance's install, and
|
||||
// the Mapbox basemap draws Middle-East places in Arabic/Persian/Hebrew script for every viewer.
|
||||
installRtlTextPlugin(gl)
|
||||
try {
|
||||
await load()
|
||||
const map = new gl.Map({
|
||||
|
|
|
|||
Loading…
Reference in a new issue