Update qkf/stepRegistry.js

This commit is contained in:
2026-02-08 21:00:42 -06:00
parent 8f22245637
commit f6eb926660

View File

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