/**
* @file
* @brief Loader implementation (Metro / React Native): reads the STATIC generated catalog
* (metroCatalogData.js) synchronously. The THIRD isomorphic loader, beside `nodeLoader` (fs +
* createRequire) and `browserLoader` (Vite glob), powering the Expo mobile target.
*
* Unlike the browser loader there is NO code-split / first-paint dance: the mobile bundle is local, so
* the whole catalog is imported eagerly and every getter is synchronous. Unlike the node loader there is
* no filesystem and no `createRequire`: Metro can't `require()` a runtime path, so `.js` block
* generators are pulled in as STATIC imports by the generated module, and `.dpl` text is compiled on
* demand here (same as the browser). It implements the identical loader interface the engine +
* suggestion builder consume, so `createEngine(metroLoader)` runs the unchanged pipeline.
*
* The catalog is produced by `scripts/build-metro-catalog.mjs` (tier full|sfw). NSFW content is absent
* from the sfw catalog, so `includeAdult` is still honoured — it simply has nothing extra to surface.
*/
import {
resolveListLines,
logicalListNames,
allListNames,
resolveName,
compareNames,
} from "../listManifest.js";
import compileDpl from "./dpl/dpl.js";
import {
dpJsModules,
dpDplText,
listLines,
groupLines,
dpGroupLines,
listMetaMap,
dpMetaMap,
presets,
blockKeys,
forcedDirs,
dpForcedDirsAll,
groupListDirs as _groupListDirs,
dpGroupDirs,
} from "./metroCatalogData.js";
let dplModCache = {};
const _listLinesCache = new Map(); // `${name}|${includeAdult}` -> string[]|null
// ---- Runtime user overlay (mobile Manage) ----------------------------------------------------------
// The mobile app's on-device user content (custom lists + block generators, edited in Manage) is injected
// here at RUNTIME via setMetroOverlay(); the loader consults it FIRST (user-wins) — the analog of the
// web runtime overlay (browserUserCatalog) and the node loader's [user, data] scan. It defaults to EMPTY,
// so the built-in catalog — and therefore metro-parity-check, which never populates it — is unaffected.
// NOTE: an overlay block's `.js` sidecar body can't execute on device (Metro has no runtime require/eval),
// so a user block that calls `insert js:` yields "" for that call — its DPL still runs. Same limitation
// the web notes flag for edited JS. The `.dpl` and lists apply live.
const overlay = {
lists: Object.create(null), // logical list name -> raw text
listMeta: Object.create(null), // list name -> meta
blocks: Object.create(null), // block key -> raw .dpl text
blockMeta: Object.create(null), // block key -> meta
};
let _overlayVersion = 0;
let _namesMemo = null;
let _namesMemoVersion = -1;
/**
* Replace the runtime user overlay and invalidate the loader caches. Call with empty/omitted maps to
* clear it (restoring the pure built-in catalog).
* @param {{lists?:object, listMeta?:object, blocks?:object, blockMeta?:object}} [next]
*/
export function setMetroOverlay(next = {}) {
overlay.lists = next.lists || Object.create(null);
overlay.listMeta = next.listMeta || Object.create(null);
overlay.blocks = next.blocks || Object.create(null);
overlay.blockMeta = next.blockMeta || Object.create(null);
_overlayVersion++;
dplModCache = {};
_listLinesCache.clear();
}
// Full list-name set (built-in + overlay), memoized until the overlay changes. Built-in-only composition
// matches the browser loader's `_allNames`; overlay list names are folded in user-wins.
function getAllNames() {
if (_namesMemo && _namesMemoVersion === _overlayVersion) return _namesMemo;
_namesMemo = allListNames([
...logicalListNames([
...Object.keys(listLines),
...Object.keys(groupLines),
...Object.keys(overlay.lists),
]),
..._groupListDirs,
]);
_namesMemoVersion = _overlayVersion;
return _namesMemo;
}
// Bridge for a compiled `.dpl`: resolve a JS sidecar (`script:` / `{js:}` / `insert js:`) from the
// static module map by joining the sidecar's relative path onto the `.dpl`'s key. Mirrors browserBridge;
// root-absolute (`/src/...`) paths aren't in the map and resolve to "" (those import internally).
function metroBridge(dplKey) {
const baseDir = dplKey.includes("/") ? dplKey.slice(0, dplKey.lastIndexOf("/")) : "";
const joinKey = (rel) => {
rel = rel.replace(/\.js$/, "");
if (rel.startsWith("/")) return null;
const parts = baseDir ? baseDir.split("/") : [];
for (const seg of rel.split("/")) {
if (seg === "." || seg === "") continue;
else if (seg === "..") parts.pop();
else parts.push(seg);
}
return parts.join("/");
};
return {
resolveJs(p, ctx) {
const k = joinKey(p);
const mod = k && dpJsModules[k];
const fn = mod && (mod.default || mod);
return typeof fn === "function"
? (fn(ctx.settings, ctx.imageSettings, ctx.upscaleSettings, ctx.intensity, ctx.focus) ?? "")
: "";
},
runPrompt: (name) => `{#${String(name).replace(/^#/, "")}}`,
runList: (name) => `{${name}}`,
expand: (s) => s,
};
}
/**
* Metro data loader for the engine — the synchronous, static-catalog analog of nodeLoader/browserLoader.
* Implements `readListLines`, `listNames`, `loadBlock`, `blockNames`, and the group/marker/meta/preset
* accessors.
* @type {object}
*/
export const metroLoader = {
readListLines(name, includeAdult = false) {
const cacheKey = `${name}|${includeAdult ? 1 : 0}|${_overlayVersion}`;
if (_listLinesCache.has(cacheKey)) return _listLinesCache.get(cacheKey);
const names = getAllNames();
const canonical = resolveName(name, names);
const lines = resolveListLines(
canonical,
{
names,
readListFile: (n) => overlay.lists[n] ?? listLines[n] ?? null, // user-wins
readGroupFile: (n) => groupLines[n] ?? null,
groupListDirs: _groupListDirs,
},
includeAdult,
);
_listLinesCache.set(cacheKey, lines);
return lines;
},
listNames() {
return getAllNames();
},
forcedPrefixDirs() {
return forcedDirs;
},
groupListDirs() {
return _groupListDirs;
},
readListMeta(name) {
return overlay.listMeta[name] ?? listMetaMap[name] ?? null;
},
loadBlock(key) {
if (dplModCache[key]) return dplModCache[key];
// Overlay block source wins over the built-in of the same key.
const src = overlay.blocks[key] ?? dpDplText[key];
if (src) {
const mod = compileDpl(src, metroBridge(key));
dplModCache[key] = mod;
return mod;
}
return dpJsModules[key] ?? null;
},
blockNames() {
return [...new Set([...blockKeys, ...Object.keys(overlay.blocks)])].sort(compareNames);
},
readBlockMeta(name) {
return overlay.blockMeta[name] ?? dpMetaMap[name] ?? null;
},
/** The raw `.dpl` source for a key (overlay-first) — used by Manage to view/override built-ins. */
readBlockSource(key) {
return overlay.blocks[key] ?? dpDplText[key] ?? null;
},
blockForcedPrefixDirs() {
return dpForcedDirsAll;
},
blockForcedPrefixDirsAll() {
return dpForcedDirsAll;
},
blockGroupDirs() {
return dpGroupDirs;
},
blockGroupDirsAll() {
return dpGroupDirs;
},
readBlockGroup(name) {
return dpGroupLines[name] ?? null;
},
presetNames() {
return Object.keys(presets);
},
loadPreset(name) {
return presets[name] ?? null;
},
};