its-compiler-js
Version:
JavaScript/TypeScript implementation of the Instruction Template Specification (ITS) compiler
246 lines • 9.34 kB
JavaScript
/**
* Variable processing and substitution for ITS Compiler
*/
import { ITSVariableError } from './types.js';
export class VariableProcessor {
/**
* Process variable references in content elements
*/
processContent(content, variables) {
return content.map(element => this.processElement(element, variables));
}
/**
* Process variables in a single content element
*/
processElement(element, variables) {
if (element.type === 'text') {
const textElement = element;
return {
...element,
text: this.processString(textElement.text, variables),
};
}
if (element.type === 'placeholder') {
const placeholderElement = element;
return {
...element,
config: this.processObject(placeholderElement.config, variables),
};
}
if (element.type === 'conditional') {
const conditionalElement = element;
return {
...element,
condition: this.processString(conditionalElement.condition, variables),
content: this.processContent(conditionalElement.content, variables),
else: conditionalElement.else ? this.processContent(conditionalElement.else, variables) : undefined,
};
}
return element;
}
/**
* Process variables in an object
*/
processObject(obj, variables) {
if (typeof obj === 'string') {
return this.processString(obj, variables);
}
if (Array.isArray(obj)) {
return obj.map(item => this.processObject(item, variables));
}
if (typeof obj === 'object' && obj !== null) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = this.processObject(value, variables);
}
return result;
}
return obj;
}
/**
* Process variable references in a string
*/
processString(text, variables) {
return text.replace(VariableProcessor.VARIABLE_PATTERN, (_match, varRef) => {
try {
const value = this.resolveVariableReference(varRef.trim(), variables);
return this.sanitiseResolvedValue(value);
}
catch (error) {
if (error instanceof ITSVariableError) {
throw error;
}
throw new ITSVariableError(`Error resolving variable reference: ${varRef}`, varRef, Object.keys(variables));
}
});
}
/**
* Resolve a variable reference like "user.name" or "items[0]"
*/
resolveVariableReference(varRef, variables) {
// Validate reference syntax
if (!this.isValidVariableReference(varRef)) {
throw new ITSVariableError(`Invalid variable reference syntax: ${varRef}`, varRef);
}
const parts = this.parseVariableReference(varRef);
let current = variables;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part.type === 'property') {
// Handle special properties
if (part.name === 'length' && (Array.isArray(current) || typeof current === 'string')) {
return current.length;
}
if (typeof current !== 'object' || current === null) {
throw new ITSVariableError(`Cannot access property '${part.name}' on non-object value`, varRef);
}
if (part.name && !(part.name in current)) {
throw new ITSVariableError(`Property '${part.name}' not found`, varRef, typeof current === 'object' ? Object.keys(current) : []);
}
if (part.name) {
current = current[part.name];
}
}
else if (part.type === 'index') {
if (!Array.isArray(current)) {
throw new ITSVariableError(`Cannot access array index on non-array value`, varRef);
}
if (part.index !== undefined) {
if (part.index < 0) {
// Support negative indexing
const actualIndex = current.length + part.index;
if (actualIndex < 0 || actualIndex >= current.length) {
throw new ITSVariableError(`Array index ${part.index} out of bounds for array of length ${current.length}`, varRef);
}
current = current[actualIndex];
}
else {
if (part.index >= current.length) {
throw new ITSVariableError(`Array index ${part.index} out of bounds for array of length ${current.length}`, varRef);
}
current = current[part.index];
}
}
}
}
return current;
}
/**
* Validate variable reference syntax
*/
isValidVariableReference(varRef) {
// Check for dangerous patterns
if (varRef.includes('..') || varRef.startsWith('_') || varRef.includes('__')) {
return false;
}
// Basic pattern matching for valid variable references
const pattern = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*|\[[0-9-]+\])*$/;
return pattern.test(varRef.replace(/\.length/g, '.length'));
}
/**
* Parse variable reference into parts
*/
parseVariableReference(varRef) {
const parts = [];
let current = '';
let i = 0;
while (i < varRef.length) {
const char = varRef[i];
if (char === '.') {
if (current) {
parts.push({ type: 'property', name: current });
current = '';
}
}
else if (char === '[') {
if (current) {
parts.push({ type: 'property', name: current });
current = '';
}
// Parse array index
i++; // Skip '['
let indexStr = '';
while (i < varRef.length && varRef[i] !== ']') {
indexStr += varRef[i];
i++;
}
if (i >= varRef.length || varRef[i] !== ']') {
throw new ITSVariableError(`Malformed array index in variable reference: ${varRef}`);
}
const index = parseInt(indexStr, 10);
if (isNaN(index)) {
throw new ITSVariableError(`Invalid array index: ${indexStr}`);
}
parts.push({ type: 'index', index });
}
else {
current += char;
}
i++;
}
if (current) {
parts.push({ type: 'property', name: current });
}
return parts;
}
/**
* Sanitise resolved variable value for safe output
*/
sanitiseResolvedValue(value) {
if (typeof value === 'string') {
return value;
}
if (Array.isArray(value)) {
// Convert arrays to comma-separated string
return value.map(item => String(item)).join(', ');
}
if (typeof value === 'object' && value !== null) {
// Convert objects to safe string representation
return `[Object with ${Object.keys(value).length} properties]`;
}
// Convert other types to string
const strValue = String(value);
if (strValue.length > 1000) {
return strValue.substring(0, 1000) + '... [TRUNCATED]';
}
return strValue;
}
/**
* Find all variable references in content
*/
findVariableReferences(content) {
const references = new Set();
const contentStr = JSON.stringify(content);
let match;
while ((match = VariableProcessor.VARIABLE_PATTERN.exec(contentStr)) !== null) {
references.add(match[1].trim());
}
return Array.from(references);
}
/**
* Validate that all variable references can be resolved
*/
validateVariables(content, variables) {
const errors = [];
const references = this.findVariableReferences(content);
for (const varRef of references) {
try {
this.resolveVariableReference(varRef, variables);
}
catch (error) {
if (error instanceof ITSVariableError) {
errors.push(error.message);
}
else if (error instanceof Error) {
errors.push(`Error validating variable reference '${varRef}': ${error.message}`);
}
else {
errors.push(`Error validating variable reference '${varRef}': ${error}`);
}
}
}
return errors;
}
}
VariableProcessor.VARIABLE_PATTERN = /\$\{([^}]+)\}/g;
//# sourceMappingURL=variable-processor.js.map