// qkf/stepRegistry.js function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function normalizeText(t) { return String(t || "").trim().replace(/\s+/g, " "); } function patternToRegex(pattern) { // Supports: // - {string} -> "value" OR 'value' OR bareWord(s) // Also normalizes whitespace const norm = normalizeText(pattern); // Split around {string} const parts = norm.split("{string}").map((p) => escapeRegex(p)); // Capture: // - "..." or '...' or unquoted token(s) (until end) // We make it non-greedy and allow extra spaces. const CAPTURE = `(?:"([^"]+)"|'([^']+)'|([^]+?))`; let regexStr = "^"; for (let i = 0; i < parts.length; i++) { regexStr += parts[i]; if (i < parts.length - 1) { // allow flexible whitespace around the capture regexStr += "\\s+" + CAPTURE + "\\s+"; } } regexStr += "$"; return new RegExp(regexStr.replace(/\s+/g, "\\s+"), "i"); } function createStepRegistry(initialCtx = {}) { const defs = []; function register(pattern, fn, meta = {}) { defs.push({ pattern: String(pattern), regex: patternToRegex(pattern), fn, meta, }); } function WHEN(pattern, fn) { register(pattern, fn, { keyword: "WHEN" }); } function AND(pattern, fn) { register(pattern, fn, { keyword: "AND" }); } function THEN(pattern, fn) { register(pattern, fn, { keyword: "THEN" }); } function GIVEN(pattern, fn){ register(pattern, fn, { keyword: "GIVEN" }); } function BUT(pattern, fn) { register(pattern, fn, { keyword: "BUT" }); } async function run(stepText, extraCtx = {}) { const text = normalizeText(stepText); for (const d of defs) { const m = d.regex.exec(text); if (!m) continue; const captures = []; for (let i = 1; i < m.length; i += 3) { const v = m[i] || m[i + 1] || m[i + 2] || ""; captures.push(normalizeText(v)); } const ctx = { ...initialCtx, ...extraCtx }; // ✅ Begin step tracking so QKF calls are awaited even if step defs don't `await` if (ctx.qkf && typeof ctx.qkf.__beginStep === "function") ctx.qkf.__beginStep(); try { // Step defs get (...captures, ctx) const res = d.fn.apply(null, [...captures, ctx]); // Await the step function itself (if it's async) await res; // ✅ Then await all QKF actions triggered during the step if (ctx.qkf && typeof ctx.qkf.__endStep === "function") await ctx.qkf.__endStep(); return; } catch (err) { // try to flush pending actions so failures surface cleanly try { if (ctx.qkf && typeof ctx.qkf.__endStep === "function") await ctx.qkf.__endStep(); } catch (_) { // ignore secondary errors } throw err; } } const available = defs.map((d) => `- ${d.pattern}`).join("\n"); throw new Error( `No step definition matched:\n "${text}"\n\nAvailable definitions:\n${available}` ); } return { defs, register, run, WHEN, AND, THEN, GIVEN, BUT, }; } module.exports = { createStepRegistry };