export interface WorkflowVariable { key: string; label: string; placeholder?: string; default?: string; required?: boolean; } const tokenRegex = () => /\{\{\s*([\w-]+)\s*\}\}/g; const placeholderRegex = () => /\[[^[\]\n]+\]/g; const humanize = (key: string) => key.replace(/[_-]+/g, ' ').trim().replace(/\b\w/g, (char) => char.toUpperCase()); /** Ordered, de-duplicated `{{key}}` tokens found in a template. */ export const parseVariableKeys = (template: string): string[] => { const keys: string[] = []; for (const match of template.matchAll(tokenRegex())) { if (!keys.includes(match[1])) keys.push(match[1]); } return keys; }; /** Merge the template's `{{key}}` tokens with any existing variable metadata. */ export const deriveVariables = ( template: string, existing: WorkflowVariable[] = [], ): WorkflowVariable[] => { const byKey = new Map(existing.map((variable) => [variable.key, variable])); return parseVariableKeys(template).map( (key) => byKey.get(key) ?? { key, label: humanize(key) }, ); }; /** * Expand a template into composer "fill" text: each `{{key}}` becomes its default * value, or a `[Label]` placeholder token the user can type over / Tab between. */ export const expandForFill = (template: string, variables: WorkflowVariable[]): string => { const byKey = new Map(variables.map((variable) => [variable.key, variable])); return template.replace(tokenRegex(), (_, key: string) => { const variable = byKey.get(key); if (variable?.default) return variable.default; return `[${variable?.label ?? humanize(key)}]`; }); }; /** Range of the first `[placeholder]` token in `text`, or null. */ export const findFirstPlaceholder = (text: string): [number, number] | null => { const match = placeholderRegex().exec(text); return match ? [match.index, match.index + match[0].length] : null; }; /** Range of the next `[placeholder]` at or after `from` (wraps to the first), or null. */ export const findNextPlaceholder = (text: string, from: number): [number, number] | null => { const matches = [...text.matchAll(placeholderRegex())]; if (matches.length === 0) return null; const next = matches.find((match) => (match.index ?? 0) >= from) ?? matches[0]; const index = next.index ?? 0; return [index, index + next[0].length]; };