src/dynPromptManifest.js

/**
 * @file
 * @brief Dynamic-prompt metadata + group/wildcard helpers — the analog of listManifest.js
 * for the `data/dynamic-prompts/` catalog. Pure data + tiny resolvers, browser-safe (no
 * Node-only imports), so it runs in Node and the Vite SPA alike.
 *
 * Like lists, a category FOLDER with 2+ generators is an IMPLIED group, but the "pick one"
 * picks ONE GENERATOR (not one word): `{#scene}` runs one random scene generator. The
 * reserved `{#any}` wildcard (and `{#any-sfw}` / `{#any-nsfw}`) picks one generator from the
 * WHOLE v2 catalog — the generator-level analog of the lists' `{keyword}` wildcard. Gating
 * is automatic by name token via gatedLists.js.
 */

import { autoGroupListDirs } from "./listManifest.js";

/**
 * Reserved wildcard base: `{#any}` (and `{#any-sfw}` / `{#any-nsfw}`) is not a file — it
 * runs one random generator drawn from the whole v2 catalog, mode-aware. Reserved like the
 * lists' `{keyword}`.
 * @type {string}
 */
export const RESERVED_ANY = "any";

/**
 * @param {string} name A dynamic-prompt reference (may carry a `-sfw`/`-nsfw` suffix).
 * @returns {boolean} Whether it is the reserved `{#any}` wildcard (any variant).
 */
export function isReservedAny(name) {
  return String(name).replace(/-(sfw|nsfw)$/i, "") === RESERVED_ANY;
}

/**
 * Per-generator tag metadata (the analog of `listTags`): a category plus anime/nsfw
 * flags, for UI badges and docs. The category is normally derivable from the folder; this
 * map only needs entries that carry extra flags. Anything absent defaults to
 * `{ anime:false, nsfw:false }`.
 * @type {Object<string, {category: (string|undefined), anime: (boolean|undefined), nsfw: (boolean|undefined)}>}
 */
export const dynPromptTags = {
  "v2/prompt/d": { category: "prompt", anime: true, nsfw: false },
};

/**
 * The category folders that are IMPLIED groups: a v2 folder with 2+ generators. Reuses the
 * list rule (`autoGroupListDirs`) over the v2 names only (v1/ is excluded — it is reached
 * via `{#name-v1}`, never grouped). Marker dirs force a folder on/off.
 * @param {string[]} names All dynamic-prompt catalog names.
 * @param {string[]} [enableDirs] Folders forced on (`_enable-group-list`).
 * @param {string[]} [disableDirs] Folders forced off (`_disable-group-list`).
 * @returns {string[]} The implied-group folder paths.
 */
export function dynGroupDirs(names, enableDirs = [], disableDirs = []) {
  return autoGroupListDirs(
    names.filter((n) => !n.startsWith("v1/")),
    enableDirs,
    disableDirs,
  );
}

/**
 * Direct-child generator names of a group folder (NOT descendants — groups don't stack).
 * @param {string} dir The folder path (e.g. "v2/scene").
 * @param {string[]} names All dynamic-prompt catalog names.
 * @returns {string[]} The member generator keys.
 */
export function dynGroupMembers(dir, names) {
  return names.filter((n) => n.startsWith(`${dir}/`) && !n.slice(dir.length + 1).includes("/"));
}