Files
mini/src/lib/spintax/resolver.ts

155 lines
4.2 KiB
TypeScript

// 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, string>): 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<string, string>
): Array<Record<string, string>> {
const keys = Object.keys(variables);
const values = keys.map(key => variables[key].split('|').map(v => v.trim()));
function* cartesian(arrays: string[][]): Generator<string[]> {
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<Record<string, string>> = [];
for (const combo of cartesian(values)) {
const obj: Record<string, string> = {};
keys.forEach((key, i) => {
obj[key] = combo[i];
});
results.push(obj);
}
return results;
}