engine/dplInsertCatalog.js

/**
 * @file The DPL **insert catalog** — the engine's own grammar, described once, for every target.
 *
 * This is the list of non-text DPL constructs (structure, chance, choose, repeat, flow, emphasis,
 * code) that the prompt box's Insert menu offers: for each one, the literal `syntax`, an editor
 * `template` and — where output is meaningful — a live `example`. Every entry mirrors a rule the DPL
 * compiler in `engine/core/dpl/dpl.js` actually implements. **It is a description of the engine's
 * language, so it belongs to the engine** — not to the web app that happened to build the first menu.
 *
 * It used to live twice: `targets/web/frontend/lib/dpl/dplInserts.js` and a 262-line hand-port in
 * `targets/mobile/lib/dplInserts.js`, kept honest by a drift check (`checkDplInserts`). That check is
 * deleted with this file's arrival — a copy that can't exist can't drift. (See
 * `notes/plans/de-duplication.md`; this was the last hand-port in the campaign.)
 *
 * ## What lives here and what does NOT
 *
 * **Here:** everything language-neutral — the ids, the DPL text, the structure. Change the grammar,
 * change it once.
 *
 * **Not here:** the human labels. The web localizes them through react-intl (`dplInsertsMessages.js`)
 * and the mobile app ships English strings; a target's label layer is a **presentation** concern with
 * different machinery on each platform. Forcing one on the other would import the web's i18n
 * dependency into React Native for no gain. So each target attaches its own labels via
 * {@link buildInsertMenu} — the shared thing is shared, the platform-specific thing stays put.
 *
 * ## Template conventions (the same in every target)
 *
 * - `${1:foo}` — a numbered tab stop with a default (CodeMirror selects it so you can type over it).
 * - `${sel}`   — the current selection, for `wrap` items (emphasis et al.).
 * - `line: true` — a line-leading construct; the editor pushes it onto a fresh line.
 * - `wrap: true` — wraps the current selection.
 * - `example`    — concrete DPL the menu re-rolls live.
 */

/**
 * The categories and their constructs. Language-neutral: ids, DPL syntax, snippet templates, examples.
 * @type {Array<{key: string, items: Array<{id: string, syntax: string, template: string, line?: boolean, wrap?: boolean, example?: string, descValues?: Record<string, string>}>}>}
 */
export const DPL_INSERT_CATALOG = [
  {
    key: "structure",
    items: [
      {
        id: "bullet",
        syntax: "- <text>",
        template: "- ${1:detail}",
        line: true,
        example: "- soft rim lighting",
      },
      {
        id: "weight",
        syntax: "[<N>] <text>",
        template: "[${1:100}] ${2:detail}",
        line: true,
        example: "[20] painted last\n[10] painted first",
      },
      {
        id: "heading",
        syntax: "Name\n===",
        template: "${1:Section Name}\n===\n${2:content}",
        line: true,
      },
    ],
  },
  {
    key: "chance",
    items: [
      {
        id: "maybe",
        syntax: "maybe <text>",
        template: "maybe ${1:detail}",
        line: true,
        example: "maybe golden hour lighting",
      },
      {
        id: "pct-chance",
        syntax: "<N>% chance <text>",
        template: "${1:30}% chance ${2:detail}",
        line: true,
        example: "30% chance golden hour lighting",
      },
      {
        id: "otherwise",
        syntax: "otherwise <text>",
        template: "otherwise ${1:detail}",
        line: true,
        example: "50% chance bright daylight\notherwise moody shadows",
      },
    ],
  },
  {
    key: "choose",
    items: [
      {
        id: "one-of",
        syntax: "one of\n  - a\n  - b",
        template: "one of\n  - ${1:option}\n  - ${2:option}",
        line: true,
        example: "one of\n  - crimson\n  - teal\n  - amber",
      },
      {
        id: "n-of",
        syntax: "<N> of\n  - a\n  - b",
        template: "${1:2} of\n  - ${2:option}\n  - ${3:option}\n  - ${4:option}",
        line: true,
        example: "2 of\n  - crimson\n  - teal\n  - amber\n  - gold",
      },
      {
        id: "range-of",
        syntax: "<N> to <M> of\n  - a\n  - b",
        template: "${1:1} to ${2:2} of\n  - ${3:option}\n  - ${4:option}",
        line: true,
        example: "1 to 2 of\n  - crimson\n  - teal\n  - amber",
      },
      {
        id: "one-of-nothing",
        syntax: "one of (<N>% nothing)\n  - a\n  - b",
        template: "one of (${1:25}% nothing)\n  - ${2:option}\n  - ${3:option}",
        line: true,
        example: "one of (40% nothing)\n  - sparkles\n  - lens flare",
      },
    ],
  },
  {
    key: "repeat",
    items: [
      {
        id: "repeat-n",
        syntax: "repeat <N> times\n  - body",
        template: "repeat ${1:2} times\n  - ${2:thing}",
        line: true,
        example: "repeat 2 times\n  - one of\n    - star\n    - swirl",
      },
      {
        id: "repeat-range",
        syntax: "repeat <N> to <M> times\n  - body",
        template: "repeat ${1:1} to ${2:3} times\n  - ${3:thing}",
        line: true,
        example: "repeat 1 to 3 times\n  - one of\n    - star\n    - swirl",
      },
    ],
  },
  {
    key: "flow",
    items: [
      { id: "goto", syntax: "go to <Section>", template: "go to ${1:Section Name}", line: true },
      { id: "goback", syntax: "go back", template: "go back", line: true },
      { id: "insert", syntax: "insert <name>", template: "insert ${1:name}", line: true },
      {
        id: "call",
        syntax: "+<name>",
        template: "+${1:name}",
        line: true,
        // The one entry whose description interpolates a value (the {#name} token it compiles to).
        descValues: { token: "{#name}" },
      },
      {
        id: "insert-js",
        syntax: "insert js: <path>",
        template: "insert js: ${1:path}",
        line: true,
      },
    ],
  },
  {
    key: "emphasis",
    items: [
      { id: "emph", syntax: "(text)", template: "(${sel})", wrap: true },
      { id: "emph-strong", syntax: "((text))", template: "((${sel}))", wrap: true },
      { id: "de-emph", syntax: "[text]", template: "[${sel}]", wrap: true },
      { id: "emph-weight", syntax: "(text:1.2)", template: "(${1:text}:${2:1.2})" },
    ],
  },
  {
    key: "code",
    items: [
      { id: "comment", syntax: "; <note>", template: "; ${1:note}", line: true },
      { id: "salt", syntax: "{salt}", template: "{salt}", example: "{salt}" },
    ],
  },
];

/**
 * `one-of-nothing` → `oneOfNothing`. The label key a target uses for an entry is derived from its id,
 * so a new construct can't be added to the grammar without a label existing for it (the coverage test
 * in `tests/unit/dplInsertCatalog.test.js` fails). One id, one name, in every target.
 * @param {string} id A catalog category key or item id.
 * @returns {string} The camelCase form.
 */
export function camelId(id) {
  return id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}

/**
 * Strip an editor template down to plain text — `${1:foo}` → `foo`, `${sel}` → nothing.
 *
 * Needed by any target without a snippet-aware editor (the mobile app has a plain `TextInput`, not
 * CodeMirror), so it inserts the concrete text instead of the tab-stop form.
 * @param {string} template The `template` field of a catalog item.
 * @returns {string} Insertable text.
 */
export function materializeTemplate(template) {
  return template
    .replace(/\$\{\d+:([^}]*)\}/g, "$1") // ${1:foo} -> foo
    .replace(/\$\{sel\}/g, "") // ${sel} -> (nothing; no selection)
    .replace(/\$\{\d+\}/g, ""); // bare ${1} -> nothing
}

/**
 * Attach a target's label layer to the shared grammar, producing the menu the UI renders.
 * @param {object} labellers
 * @param {(category: object) => {label: string, hint: string}} labellers.category Labels for a category.
 * @param {(item: object, category: object) => {label: string, desc: string}} labellers.item Labels for an item.
 * @returns {Array<object>} The catalog with `label`/`hint`/`desc` filled in.
 */
export function buildInsertMenu({ category, item }) {
  return DPL_INSERT_CATALOG.map((c) => ({
    ...c,
    ...category(c),
    items: c.items.map((it) => ({ ...it, ...item(it, c) })),
  }));
}

export default DPL_INSERT_CATALOG;