// JS adapter: loads the REAL grammar out of src/engine.js (no copy) and emits the // neutral normalized structure defined in docs/track-format.md §5. // // engine.js has no top-level side effects (only const/function declarations) and references // `window` only inside ensureAudio(), which we never call — so it loads cleanly in Node. import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const here = dirname(fileURLToPath(import.meta.url)); const src = readFileSync(join(here, "..", "..", "src", "engine.js"), "utf8"); // Expose the pure codec functions from engine.js's top-level scope. globalThis.window = globalThis.window || {}; const api = new Function(src + "\n;return { patchToSetup, setupToPatch };")(); const groupsArr = (s) => String(s).split(/[^0-9]+/).filter(Boolean).map(Number); export function normalize(patch) { const s = api.patchToSetup(patch); return { bpm: s.bpm, bars: s.bars || 0, volume: s.volume == null ? null : s.volume, countMs: s.countMs || 0, ramp: s.ramp && s.ramp.on ? { start: s.ramp.startBpm, amt: s.ramp.amount, every: s.ramp.everyBars } : null, trainer: s.trainer && s.trainer.on ? { play: s.trainer.playBars, mute: s.trainer.muteBars } : null, end: s.end == null ? null : s.end, rep: s.end == null ? null : (s.rep == null ? 1 : s.rep), // rep only meaningful with end; defaults to 1 lanes: (s.lanes || []).map((c) => ({ sound: c.sound, groups: groupsArr(c.groupsStr), sub: c.stepsPerBeat || 1, swing: !!c.swing, poly: !!c.poly, mute: c.enabled === false, gainDb: c.gainDb || 0, levels: (c.beatsOn || []).map((v) => v | 0), orns: (c.orns || []).map((v) => v | 0), })), }; } // serialize(parse(x)) — used for the idempotency check. export function canonical(patch) { return api.setupToPatch(api.patchToSetup(patch)); } // CLI: `node js_adapter.mjs ''` prints normalized JSON. if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { process.stdout.write(JSON.stringify(normalize(process.argv[2] ?? ""))); }