// Extracts the combo table and label maps from reference/index.html into TypeScript. // // The reference is the data spec, so the 159 entries are never retyped by hand: // this lifts the literals straight out of it and re-runs whenever it changes. // It reads only pure-data declarations, evaluated in a vm with no DOM. import { readFileSync, writeFileSync } from "node:fs"; import vm from "node:vm"; const SRC = "reference/index.html"; const src = readFileSync(SRC, "utf8"); // Balance brackets from a declaration's first opener, skipping string literals so // that braces or quotes inside descriptions cannot end the scan early. function literalAfter(name) { const at = src.indexOf(`const ${name}=`); if (at < 0) throw new Error(`declaration not found: ${name}`); let i = src.indexOf("=", at) + 1; while (/\s/.test(src[i])) i++; const start = i; let depth = 0, quote = null; for (; i < src.length; i++) { const c = src[i]; if (quote) { if (c === "\\") { i++; continue; } if (c === quote) quote = null; continue; } if (c === '"' || c === "'" || c === "`") { quote = c; continue; } if (c === "{" || c === "[" || c === "(") depth++; else if (c === "}" || c === "]" || c === ")") { depth--; if (depth === 0) return src.slice(start, i + 1); } } throw new Error(`unbalanced literal: ${name}`); } const NAMES = ["L", "PL", "GROUPS", "GNAME", "NOLATCH", "MODORDER", "RAW", "CTX", "CTXHINT"]; const ctx = vm.createContext({}); for (const n of NAMES) vm.runInContext(`var ${n} = ${literalAfter(n)};`, ctx); // The reference adds the 16 pad labels in a loop right after declaring L. vm.runInContext(`for (let i = 1; i <= 16; i++) L["p" + i] = "PAD " + i;`, ctx); const g = (n) => vm.runInContext(n, ctx); const header = (what) => `// GENERATED from ${SRC} by scripts/extract-reference-data.mjs — do not edit by hand.\n` + `// ${what}\n\n`; const j = (v) => JSON.stringify(v, null, 2); writeFileSync( "src/domain/raw.ts", header("The combo table, exactly as the reference declares it.") + `import type { RawEntry } from "./types";\n\nexport const RAW: RawEntry[] = ${j(g("RAW"))};\n` ); writeFileSync( "src/domain/labels.ts", header("Control labels, groupings and context names.") + `import type { ControlId, ContextCode, GroupId } from "./types";\n\n` + `/** Full name printed on each control. */\n` + `export const L: Record = ${j(g("L"))};\n\n` + `/** Shorter labels used on the panel where the full one will not fit. */\n` + `export const PL: Partial> = ${j(g("PL"))};\n\n` + `/** A target may name a whole group of controls rather than one. */\n` + `export const GROUPS: Record = ${j(g("GROUPS"))};\n\n` + `export const GNAME: Record = ${j(g("GNAME"))};\n\n` + `/** Controls that never latch when held: the knobs and the pads. */\n` + `export const NOLATCH: ControlId[] = ${j([...g("NOLATCH")])};\n\n` + `/** Canonical modifier order, so a layer key is stable however it was pressed. */\n` + `export const MODORDER: ControlId[] = ${j(g("MODORDER"))};\n\n` + `export const CTX: Record = ${j(g("CTX"))};\n\n` + `export const CTXHINT: Record = ${j(g("CTXHINT"))};\n` ); const raw = g("RAW"); console.log(`RAW entries ${raw.length}`); console.log(`control labels ${Object.keys(g("L")).length}`); console.log(`contexts ${Object.keys(g("CTX")).join(", ")}`); console.log(`groups ${Object.keys(g("GROUPS")).join(", ")}`); console.log(`entries with how ${raw.filter((r) => r.length > 7).length}`); console.log(`group targets ${raw.filter((r) => typeof r[1] === "string" && g("GROUPS")[r[1]]).length}`); console.log(`array targets ${raw.filter((r) => Array.isArray(r[1])).length}`);