diff --git a/qkf/stepRegistry.js b/qkf/stepRegistry.js index e7734a9..fec0504 100644 --- a/qkf/stepRegistry.js +++ b/qkf/stepRegistry.js @@ -1,87 +1,111 @@ -// qkf/stepRegistry.js - -function escapeRegex(s) { - return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function normalizeText(t) { - return String(t || "").trim().replace(/\s+/g, " "); -} - -function patternToRegex(pattern) { - const parts = String(pattern).split("{string}"); - const escaped = parts.map((p) => escapeRegex(normalizeText(p))); - const joined = escaped.join("\\s+(.+?)\\s+"); - const final = "^" + joined.replace(/\s+/g, "\\s+") + "$"; - return new RegExp(final, "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 = m.slice(1).map((x) => normalizeText(x)); - 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 }; +// 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 };