// Spintax Resolver - Handles {A|B|C} syntax import crypto from 'crypto'; export interface SpintaxChoice { path: string; chosen: string; allOptions: string[]; } /** * Resolves spintax syntax {A|B|C} to a single choice * Tracks which choices were made for uniqueness */ export class SpintaxResolver { private choices: SpintaxChoice[] = []; private seed: string; constructor(seed?: string) { this.seed = seed || crypto.randomBytes(16).toString('hex'); } /** * Resolve all spintax in text */ resolve(text: string): string { let resolved = text; let iteration = 0; // Handle nested spintax with multiple passes while (resolved.includes('{') && resolved.includes('}') && iteration < 10) { resolved = this.resolvePass(resolved, iteration); iteration++; } return resolved; } private resolvePass(text: string, iteration: number): string { const regex = /\{([^{}]+)\}/g; let result = text; let match; let offset = 0; while ((match = regex.exec(text)) !== null) { const fullMatch = match[0]; const options = match[1].split('|'); // Deterministic choice based on seed + position const choiceIndex = this.getChoiceIndex(match.index + iteration, options.length); const chosen = options[choiceIndex].trim(); // Track the choice this.choices.push({ path: `pos_${match.index}_iter_${iteration}`, chosen, allOptions: options }); // Replace in result const beforeMatch = result.substring(0, match.index + offset); const afterMatch = result.substring(match.index + offset + fullMatch.length); result = beforeMatch + chosen + afterMatch; offset += chosen.length - fullMatch.length; } return result; } private getChoiceIndex(position: number, optionsCount: number): number { const hash = crypto.createHash('sha256') .update(`${this.seed}_${position}`) .digest('hex'); return parseInt(hash.substring(0, 8), 16) % optionsCount; } /** * Get all choices made during resolution */ getChoices(): SpintaxChoice[] { return this.choices; } /** * Generate hash of choices for uniqueness checking */ getChoicesHash(): string { const choiceString = this.choices .map(c => c.chosen) .join('::'); return crypto.createHash('sha256') .update(choiceString) .digest('hex') .substring(0, 32); } /** * Reset for new resolution */ reset(newSeed?: string) { this.choices = []; if (newSeed) this.seed = newSeed; } } /** * Expand variables like {{CITY}} in text */ export function expandVariables(text: string, variables: Record): string { let result = text; for (const [key, value] of Object.entries(variables)) { const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g'); result = result.replace(regex, value); } return result; } /** * Generate cartesian product of pipe-separated values * Example: { CITY: "A|B", STATE: "X|Y" } => 4 combinations */ export function generateCartesianProduct( variables: Record ): Array> { const keys = Object.keys(variables); const values = keys.map(key => variables[key].split('|').map(v => v.trim())); function* cartesian(arrays: string[][]): Generator { if (arrays.length === 0) { yield []; return; } const [first, ...rest] = arrays; for (const value of first) { for (const combo of cartesian(rest)) { yield [value, ...combo]; } } } const results: Array> = []; for (const combo of cartesian(values)) { const obj: Record = {}; keys.forEach((key, i) => { obj[key] = combo[i]; }); results.push(obj); } return results; }