aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
344 lines (290 loc) • 9.9 kB
JavaScript
/**
* Handlebars Renderer Module
* Handles template rendering with custom helpers
*
* @module renderer
* @version 2.0.0
*/
;
const Handlebars = require('handlebars');
/**
* Register default Handlebars helpers
* @param {Object} handlebars - Handlebars instance
*/
function registerDefaultHelpers(handlebars) {
// Pad number with leading zeros
handlebars.registerHelper('padNumber', (num, length) => {
return String(num).padStart(length, '0');
});
// Format date with pattern
handlebars.registerHelper('formatDate', (date, format) => {
const d = date instanceof Date ? date : new Date(date);
const patterns = {
'YYYY': d.getFullYear(),
'MM': String(d.getMonth() + 1).padStart(2, '0'),
'DD': String(d.getDate()).padStart(2, '0'),
'HH': String(d.getHours()).padStart(2, '0'),
'mm': String(d.getMinutes()).padStart(2, '0'),
'ss': String(d.getSeconds()).padStart(2, '0'),
};
let result = format;
for (const [pattern, value] of Object.entries(patterns)) {
result = result.replace(pattern, value);
}
return result;
});
// Add numbers
handlebars.registerHelper('add', (a, b) => a + b);
// Subtract numbers
handlebars.registerHelper('subtract', (a, b) => a - b);
// Multiply numbers
handlebars.registerHelper('multiply', (a, b) => a * b);
// Divide numbers
handlebars.registerHelper('divide', (a, b) => a / b);
// Check equality
handlebars.registerHelper('eq', (a, b) => a === b);
// Check not equal
handlebars.registerHelper('ne', (a, b) => a !== b);
// Greater than
handlebars.registerHelper('gt', (a, b) => a > b);
// Greater than or equal
handlebars.registerHelper('gte', (a, b) => a >= b);
// Less than
handlebars.registerHelper('lt', (a, b) => a < b);
// Less than or equal
handlebars.registerHelper('lte', (a, b) => a <= b);
// Logical AND
handlebars.registerHelper('and', (...args) => {
args.pop(); // Remove options object
return args.every(Boolean);
});
// Logical OR
handlebars.registerHelper('or', (...args) => {
args.pop(); // Remove options object
return args.some(Boolean);
});
// Logical NOT
handlebars.registerHelper('not', (value) => !value);
// Uppercase string
handlebars.registerHelper('uppercase', (str) => {
return str ? String(str).toUpperCase() : '';
});
// Lowercase string
handlebars.registerHelper('lowercase', (str) => {
return str ? String(str).toLowerCase() : '';
});
// Capitalize first letter
handlebars.registerHelper('capitalize', (str) => {
if (!str) return '';
const s = String(str);
return s.charAt(0).toUpperCase() + s.slice(1);
});
// Title case string
handlebars.registerHelper('titlecase', (str) => {
if (!str) return '';
return String(str)
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
});
// Join array with separator
handlebars.registerHelper('join', (array, separator) => {
if (!Array.isArray(array)) return '';
return array.join(typeof separator === 'string' ? separator : ', ');
});
// Get array length
handlebars.registerHelper('length', (array) => {
return Array.isArray(array) ? array.length : 0;
});
// Check if array includes value
handlebars.registerHelper('includes', (array, value) => {
return Array.isArray(array) && array.includes(value);
});
// First element of array
handlebars.registerHelper('first', (array) => {
return Array.isArray(array) && array.length > 0 ? array[0] : null;
});
// Last element of array
handlebars.registerHelper('last', (array) => {
return Array.isArray(array) && array.length > 0 ? array[array.length - 1] : null;
});
// Default value if undefined/null
handlebars.registerHelper('default', (value, defaultValue) => {
return value ?? defaultValue;
});
// JSON stringify
handlebars.registerHelper('json', (value, indent) => {
return JSON.stringify(value, null, typeof indent === 'number' ? indent : 2);
});
// Repeat block n times (with @index, @first, @last data variables)
handlebars.registerHelper('times', (n, options) => {
let result = '';
for (let i = 0; i < n; i++) {
// Create data frame to expose @index, @first, @last like built-in each
const data = options.data ? Handlebars.createFrame(options.data) : {};
data.index = i;
data.first = i === 0;
data.last = i === n - 1;
result += options.fn({ index: i, first: i === 0, last: i === n - 1 }, { data });
}
return result;
});
// Conditional block based on equality
handlebars.registerHelper('ifEqual', function(a, b, options) {
return a === b ? options.fn(this) : options.inverse(this);
});
// Conditional block based on inequality
handlebars.registerHelper('unlessEqual', function(a, b, options) {
return a !== b ? options.fn(this) : options.inverse(this);
});
// Slug helper (URL-safe string)
handlebars.registerHelper('slug', (str) => {
if (!str) return '';
return String(str)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
});
// Truncate string
handlebars.registerHelper('truncate', (str, length, suffix) => {
if (!str) return '';
const s = String(str);
if (s.length <= length) return s;
return s.slice(0, length) + (typeof suffix === 'string' ? suffix : '...');
});
// Now helper for current date
handlebars.registerHelper('now', () => new Date());
}
/**
* TemplateRenderer class for rendering Handlebars templates
*/
class TemplateRenderer {
/**
* Create a TemplateRenderer instance
* @param {Object} options - Configuration options
*/
constructor(options = {}) {
this.handlebars = Handlebars.create();
this.customHelpers = new Map();
registerDefaultHelpers(this.handlebars);
// Register any custom helpers from options
if (options.helpers) {
for (const [name, fn] of Object.entries(options.helpers)) {
this.registerHelper(name, fn);
}
}
}
/**
* Register a custom Handlebars helper
* @param {string} name - Helper name
* @param {Function} fn - Helper function
*/
registerHelper(name, fn) {
this.handlebars.registerHelper(name, fn);
this.customHelpers.set(name, fn);
}
/**
* Register a partial template
* @param {string} name - Partial name
* @param {string} content - Partial content
*/
registerPartial(name, content) {
this.handlebars.registerPartial(name, content);
}
/**
* Render a template with context
* @param {Object} template - Template object with body
* @param {Object} context - Variable values for rendering
* @returns {string} Rendered content
*/
render(template, context = {}) {
const templateBody = typeof template === 'string' ? template : template.body;
try {
const compiledTemplate = this.handlebars.compile(templateBody, {
strict: false,
noEscape: true,
});
// Add metadata to context if available
const fullContext = {
...context,
_template: template.metadata || {},
now: new Date(),
};
return compiledTemplate(fullContext);
} catch (error) {
throw new Error(`Template rendering failed: ${error.message}`);
}
}
/**
* Render a template string directly
* @param {string} templateString - Template string
* @param {Object} context - Variable values for rendering
* @returns {string} Rendered content
*/
renderString(templateString, context = {}) {
return this.render(templateString, context);
}
/**
* Validate template syntax
* @param {string} templateBody - Template content to validate
* @returns {Object} Validation result with isValid and errors
*/
validateSyntax(templateBody) {
try {
const compiled = this.handlebars.compile(templateBody);
// Also try to execute with empty context to catch parse errors
// that only manifest at execution time (e.g., incomplete expressions)
compiled({});
return { isValid: true, errors: [] };
} catch (error) {
return {
isValid: false,
errors: [error.message],
};
}
}
/**
* Extract variable references from template
* @param {string} templateBody - Template content
* @returns {Array<string>} List of variable names used in template
*/
extractVariables(templateBody) {
const variables = new Set();
// Match {{variable}}, {{variable.property}}, {{#each variable}}, etc.
const regex = /\{\{[#/]?(?:each|if|unless|with)?\s*([a-zA-Z_][a-zA-Z0-9_.]*)/g;
let match;
while ((match = regex.exec(templateBody)) !== null) {
const variable = match[1].split('.')[0];
// Exclude helpers and special keywords
if (!this.isHelper(variable) && !this.isKeyword(variable)) {
variables.add(variable);
}
}
return Array.from(variables);
}
/**
* Check if name is a registered helper
* @param {string} name - Name to check
* @returns {boolean} True if helper exists
*/
isHelper(name) {
return this.customHelpers.has(name) ||
['padNumber', 'formatDate', 'add', 'subtract', 'eq', 'ne', 'gt', 'lt',
'gte', 'lte', 'and', 'or', 'not', 'uppercase', 'lowercase', 'capitalize',
'titlecase', 'join', 'length', 'includes', 'first', 'last', 'default',
'json', 'times', 'ifEqual', 'unlessEqual', 'slug', 'truncate', 'now',
'multiply', 'divide'].includes(name);
}
/**
* Check if name is a Handlebars keyword
* @param {string} name - Name to check
* @returns {boolean} True if keyword
*/
isKeyword(name) {
return ['this', '@index', '@key', '@first', '@last', '@root'].includes(name);
}
}
module.exports = {
TemplateRenderer,
registerDefaultHelpers,
};