#!/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 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 './lib/playwright-guarded.mjs' 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 = ` ` 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.`)