// qkf/stepRegistry.js function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function normalizeText(t) { return String(t || "").trim().replace(/\s+/g, " "); } function literalToRegex(lit) { // Escape + make spaces flexible return escapeRegex(normalizeText(lit)).replace(/\s+/g, "\\s+"); } /** * Pattern supports "{string}" placeholders. * Matches: * - "quoted value" * - 'quoted value' * - unquoted value (single token or multi-word) * * Unquoted capture: * - if there is a next literal => capture lazily until that literal (lookahead) * - if last placeholder => capture rest of line */ function patternToRegex(pattern) { const parts = String(pattern).split("{string}"); const litParts = parts.map((p) => literalToRegex(p)); // Each placeholder yields 3 capturing groups: dbl, sgl, unquoted const QUOTED_OR_UNQUOTED = (nextLiteralRegex) => { const dbl = `"([^"]+)"`; const sgl = `'([^']+)'`; if (nextLiteralRegex && nextLiteralRegex.length > 0) { // Capture up to the next literal (lazy) without consuming it // Allow optional whitespace before the next literal const unq = `(.+?)(?=\\s*${nextLiteralRegex})`; return `(?:${dbl}|${sgl}|${unq})`; } // Last placeholder: capture rest of line const unqLast = `(.+)`; return `(?:${dbl}|${sgl}|${unqLast})`; }; let re = "^\\s*"; for (let i = 0; i < litParts.length; i++) { const lit = litParts[i]; // Add the literal segment (if any) if (lit) re += lit; // If a placeholder follows this literal: if (i < litParts.length - 1) { const nextLit = litParts[i + 1]; // Allow whitespace between literal and value re += "\\s+"; re += QUOTED_OR_UNQUOTED(nextLit); // IMPORTANT: whitespace after value is optional; next literal (if any) will handle it re += "\\s*"; } } re += "\\s*$"; return new RegExp(re, "i"); } function createStepRegistry(initialCtx = {}) { const defs = []; function register(pattern, fn, meta = {}) { defs.push({ pattern: String(pattern), regex: patternToRegex(pattern), fn, meta, }); } // DSL helpers 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; // Each {string} adds 3 capture groups: dbl, sgl, unquoted 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 }; // Auto-await QKF calls (if you implemented these hooks) if (ctx.qkf && typeof ctx.qkf.__beginStep === "function") ctx.qkf.__beginStep(); try { const res = d.fn.apply(null, [...captures, ctx]); await res; if (ctx.qkf && typeof ctx.qkf.__endStep === "function") { await ctx.qkf.__endStep(); } return; } catch (err) { try { if (ctx.qkf && typeof ctx.qkf.__endStep === "function") { await ctx.qkf.__endStep(); } } catch (_) {} throw err; } } const available = defs.map((x) => `- ${x.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 };