@syntropiq/xtrax
Version:
XTRAX - Serverless-ready TypeScript library for data processing and regex-based parsing applications
116 lines • 4.33 kB
JavaScript
/**
* Process contents of variables.json, in preparation for recursive substitution:
*
* - Strip keys ending in '#', which are treated as comments
* - Flatten nested dicts, so {"page": {"": "A", "foo": "B"}} becomes {"page": "A", "page_foo": "B"}
* - Add optional variants for each key, so {"page": "\\d+"} becomes {"page_optional": "(?:\\d+ ?)?"}
* - Resolve nested references safely with circular reference detection
*/
export function processVariables(variables) {
// Flatten variables and remove comments
function flatten(d, parentKey = '') {
const items = {};
for (const [k, v] of Object.entries(d)) {
// Skip comment keys
if (k.endsWith('#')) {
continue;
}
const newKey = [parentKey, k].filter(Boolean).join('_');
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
// Recursively flatten nested objects
Object.assign(items, flatten(v, newKey));
}
else {
// Convert to string and store
items[newKey] = String(v);
}
}
return items;
}
let processedVariables = flatten(variables);
// Add optional variables - wrap each variable in optional group
const optionalVars = {};
for (const [k, v] of Object.entries(processedVariables)) {
optionalVars[`${k}_optional`] = `(?:${v} ?)?`;
}
processedVariables = { ...processedVariables, ...optionalVars };
// Resolve references safely - detect cycles and skip problematic variables
const resolvedVariables = {};
for (const [k, v] of Object.entries(processedVariables)) {
try {
resolvedVariables[k] = recursiveSubstitute(v, processedVariables);
}
catch (error) {
// If we hit max depth (circular reference), just use the original value
console.warn(`Circular reference detected for variable '${k}': ${v}`);
resolvedVariables[k] = v;
}
}
return resolvedVariables;
}
/**
* Recursively substitute values in `template` from `variables`. For example:
* recursiveSubstitute("$a $b $c", {'a': '$b', 'b': '$c', 'c': 'foo'})
* Result: "foo foo foo"
*
* Infinite loops will be detected after maxDepth iterations and the function
* will return the partially resolved result.
*/
export function recursiveSubstitute(template, variables, maxDepth = 100) {
let oldVal = template;
for (let i = 0; i < maxDepth; i++) {
// Replace variables in the format $var or ${var}
const newVal = oldVal.replace(/\$\{?(\w+)\}?/g, (match, varName) => {
return variables[varName] || match;
});
// If no changes were made, we're done
if (newVal === oldVal) {
break;
}
oldVal = newVal;
}
// Don't throw error for unresolved variables - just return what we have
// This matches the Python behavior where unresolved variables are left as-is
return oldVal;
}
/**
* Process variables with detailed result information
*/
export function processVariablesWithResult(variables) {
const startTime = Date.now();
const originalCount = countVariables(variables);
const processed = processVariables(variables);
const endTime = Date.now();
const processedCount = Object.keys(processed).length;
return {
variables: processed,
stats: {
originalVariableCount: originalCount,
processedVariableCount: processedCount,
processingTimeMs: endTime - startTime,
optionalVariablesAdded: Math.floor((processedCount - originalCount) / 2) // Rough estimate
}
};
}
/**
* Count variables in nested structure
*/
function countVariables(variables) {
let count = 0;
function countRecursive(obj) {
for (const [key, value] of Object.entries(obj)) {
if (key.endsWith('#')) {
continue; // Skip comments
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
countRecursive(value);
}
else {
count++;
}
}
}
countRecursive(variables);
return count;
}
//# sourceMappingURL=variable-processor.js.map