Asynchronous templates for the browser and server (LinkedIn fork)
Extensible helper system for implementing complex template logic and custom functionality through user-defined helper functions.
System for registering and managing custom helper functions that extend template capabilities.
/**
* Container for all registered helpers
* Empty by default - all helpers must be added by users
*/
const helpers: { [helperName: string]: HelperFunction };
/**
* Helper function signature
* @param chunk - Output chunk for writing content
* @param context - Template context for data access
* @param bodies - Template bodies (block, else, etc.)
* @param params - Parameters passed to helper
* @returns Modified chunk
*/
type HelperFunction = (
chunk: Chunk,
context: Context,
bodies: Bodies,
params: Params
) => Chunk;
interface Bodies {
/** Main template body */
block?: TemplateFunction;
/** Else template body */
else?: TemplateFunction;
/** Named bodies for custom helpers */
[bodyName: string]: TemplateFunction | undefined;
}
interface Params {
/** Parameters passed to helper as key-value pairs */
[paramName: string]: any;
}Usage Examples:
const dust = require('dustjs-linkedin');
// Register a simple helper
dust.helpers.uppercase = (chunk, context, bodies, params) => {
if (bodies.block) {
return chunk.capture(bodies.block, context, (data, chunk) => {
return chunk.write(data.toUpperCase());
});
}
return chunk;
};
// Register a helper with parameters
dust.helpers.repeat = (chunk, context, bodies, params) => {
const times = parseInt(params.times) || 1;
if (bodies.block) {
for (let i = 0; i < times; i++) {
chunk.render(bodies.block, context.push({ index: i }));
}
}
return chunk;
};
// Register a conditional helper
dust.helpers.compare = (chunk, context, bodies, params) => {
const left = params.left;
const right = params.right;
const operator = params.operator || 'eq';
let result = false;
switch (operator) {
case 'eq': result = left == right; break;
case 'ne': result = left != right; break;
case 'gt': result = left > right; break;
case 'lt': result = left < right; break;
case 'gte': result = left >= right; break;
case 'lte': result = left <= right; break;
}
if (result && bodies.block) {
return chunk.render(bodies.block, context);
} else if (!result && bodies.else) {
return chunk.render(bodies.else, context);
}
return chunk;
};
// Usage in templates:
// {@uppercase}hello world{/uppercase} → HELLO WORLD
// {@repeat times="3"}Item {index}{/repeat} → Item 0Item 1Item 2
// {@compare left=age right=18 operator="gte"}Adult{:else}Minor{/compare}Helpers have full access to the template context and can manipulate data resolution.
interface Context {
/** Get data from context */
get(path: string): any;
/** Get current context data */
current(): any;
/** Push new context data */
push(data: any): Context;
/** Template name for debugging */
getTemplateName(): string | undefined;
}Usage Examples:
// Helper that accesses context data
dust.helpers.userInfo = (chunk, context, bodies, params) => {
const user = context.get('user');
const currentData = context.current();
if (user) {
const info = `User: ${user.name} (${user.role})`;
return chunk.write(info);
}
return chunk.write('No user information');
};
// Helper that modifies context
dust.helpers.withDefaults = (chunk, context, bodies, params) => {
const defaults = params.defaults || {};
const enhanced = context.push(defaults);
if (bodies.block) {
return chunk.render(bodies.block, enhanced);
}
return chunk;
};
// Helper that creates isolated context
dust.helpers.isolate = (chunk, context, bodies, params) => {
const isolatedData = params.data || {};
const isolatedContext = dust.context(isolatedData);
if (bodies.block) {
return chunk.render(bodies.block, isolatedContext);
}
return chunk;
};Helpers work with chunks to control template output and handle asynchronous operations.
interface Chunk {
/** Write string content to output */
write(data: string): Chunk;
/** End chunk with optional final content */
end(data?: string): Chunk;
/** Render template body with context */
render(body: TemplateFunction, context: Context): Chunk;
/** Capture rendered content for processing */
capture(body: TemplateFunction, context: Context, callback: (data: string, chunk: Chunk) => Chunk): Chunk;
/** Create asynchronous chunk for async operations */
map(callback: (chunk: Chunk) => Chunk): Chunk;
/** Set error on chunk */
setError(error: Error): Chunk;
}Usage Examples:
// Helper that manipulates output
dust.helpers.wrap = (chunk, context, bodies, params) => {
const tag = params.tag || 'div';
const className = params.class || '';
chunk.write(`<${tag}${className ? ` class="${className}"` : ''}>`);
if (bodies.block) {
chunk.render(bodies.block, context);
}
return chunk.write(`</${tag}>`);
};
// Helper that processes content
dust.helpers.markdown = (chunk, context, bodies, params) => {
if (bodies.block) {
return chunk.capture(bodies.block, context, (data, chunk) => {
// Pseudo-code: convert markdown to HTML
const html = convertMarkdownToHtml(data);
return chunk.write(html);
});
}
return chunk;
};
// Asynchronous helper
dust.helpers.asyncData = (chunk, context, bodies, params) => {
const url = params.url;
return chunk.map((chunk) => {
// Async operation (pseudo-code)
fetchData(url, (err, data) => {
if (err) {
chunk.setError(err);
} else {
const dataContext = context.push({ asyncData: data });
if (bodies.block) {
chunk.render(bodies.block, dataContext);
}
}
chunk.end();
});
return chunk;
});
};Patterns for implementing conditional logic in templates.
// Simple existence check helper
dust.helpers.ifExists = (chunk, context, bodies, params) => {
const value = context.get(params.key);
if (value !== undefined && value !== null && value !== '') {
if (bodies.block) {
return chunk.render(bodies.block, context);
}
} else {
if (bodies.else) {
return chunk.render(bodies.else, context);
}
}
return chunk;
};
// Multi-condition helper
dust.helpers.when = (chunk, context, bodies, params) => {
const conditions = params.conditions || [];
for (const condition of conditions) {
if (evaluateCondition(condition, context)) {
if (bodies[condition.body]) {
return chunk.render(bodies[condition.body], context);
}
}
}
if (bodies.else) {
return chunk.render(bodies.else, context);
}
return chunk;
};
// Usage in templates:
// {@ifExists key="user.email"}Has email{:else}No email{/ifExists}Helpers for transforming and processing data within templates.
// Array manipulation helper
dust.helpers.slice = (chunk, context, bodies, params) => {
const array = context.get(params.array) || [];
const start = parseInt(params.start) || 0;
const end = params.end ? parseInt(params.end) : array.length;
const sliced = array.slice(start, end);
const sliceContext = context.push({ items: sliced });
if (bodies.block) {
return chunk.render(bodies.block, sliceContext);
}
return chunk;
};
// Object manipulation helper
dust.helpers.pick = (chunk, context, bodies, params) => {
const obj = context.get(params.from) || {};
const keys = params.keys ? params.keys.split(',') : [];
const picked = {};
keys.forEach(key => {
if (obj.hasOwnProperty(key.trim())) {
picked[key.trim()] = obj[key.trim()];
}
});
const pickedContext = context.push(picked);
if (bodies.block) {
return chunk.render(bodies.block, pickedContext);
}
return chunk;
};
// String manipulation helper
dust.helpers.truncate = (chunk, context, bodies, params) => {
const text = params.text || '';
const length = parseInt(params.length) || 50;
const suffix = params.suffix || '...';
const truncated = text.length > length
? text.substring(0, length) + suffix
: text;
return chunk.write(truncated);
};Helpers for formatting data output in templates.
// Date formatting helper
dust.helpers.formatDate = (chunk, context, bodies, params) => {
const date = new Date(params.date);
const format = params.format || 'YYYY-MM-DD';
if (isNaN(date.getTime())) {
return chunk.write('Invalid Date');
}
// Pseudo-code: format date according to format string
const formatted = formatDate(date, format);
return chunk.write(formatted);
};
// Number formatting helper
dust.helpers.formatNumber = (chunk, context, bodies, params) => {
const number = parseFloat(params.number);
const decimals = parseInt(params.decimals) || 2;
const locale = params.locale || 'en-US';
if (isNaN(number)) {
return chunk.write('Invalid Number');
}
const formatted = number.toLocaleString(locale, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
return chunk.write(formatted);
};
// Currency formatting helper
dust.helpers.formatCurrency = (chunk, context, bodies, params) => {
const amount = parseFloat(params.amount);
const currency = params.currency || 'USD';
const locale = params.locale || 'en-US';
if (isNaN(amount)) {
return chunk.write('Invalid Amount');
}
const formatted = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(amount);
return chunk.write(formatted);
};Helpers can define multiple named bodies for complex template structures.
dust.helpers.switch = (chunk, context, bodies, params) => {
const value = context.get(params.value);
const cases = params.cases || {};
// Look for matching case body
if (cases[value] && bodies[cases[value]]) {
return chunk.render(bodies[cases[value]], context);
}
// Fall back to default
if (bodies.default) {
return chunk.render(bodies.default, context);
}
return chunk;
};
// Usage in template:
// {@switch value=status cases.active="activeBody" cases.inactive="inactiveBody"}
// {<activeBody}Status is active{/activeBody}
// {<inactiveBody}Status is inactive{/inactiveBody}
// {<default}Unknown status{/default}
// {/switch}Best practices for handling errors within helper functions.
dust.helpers.safeHelper = (chunk, context, bodies, params) => {
try {
// Potentially error-prone operation
const result = riskyOperation(params.input);
return chunk.write(result);
} catch (err) {
// Log error for debugging
dust.log('Helper error: ' + err.message, 'ERROR');
// Render fallback content
if (bodies.error) {
return chunk.render(bodies.error, context.push({ error: err.message }));
}
// Or set error on chunk
return chunk.setError(err);
}
};
// Helper that validates parameters
dust.helpers.requireParams = (chunk, context, bodies, params) => {
const required = ['name', 'email'];
const missing = required.filter(param => !params[param]);
if (missing.length > 0) {
const error = new Error(`Missing required parameters: ${missing.join(', ')}`);
return chunk.setError(error);
}
if (bodies.block) {
return chunk.render(bodies.block, context);
}
return chunk;
};Patterns for testing custom helpers in isolation.
// Test helper function
function testHelper() {
const mockChunk = {
output: '',
write(data) { this.output += data; return this; },
render(body, ctx) { /* mock implementation */ return this; },
end() { return this; }
};
const mockContext = dust.context({ test: 'data' });
const mockBodies = { block: () => 'rendered content' };
const mockParams = { param1: 'value1' };
// Test helper
const result = dust.helpers.myHelper(mockChunk, mockContext, mockBodies, mockParams);
console.log('Helper output:', mockChunk.output);
return result;
}// Well-structured helper example
dust.helpers.bestPracticeHelper = (chunk, context, bodies, params) => {
// 1. Validate required parameters
if (!params.required) {
return chunk.setError(new Error('Missing required parameter'));
}
// 2. Sanitize inputs
const sanitized = sanitizeInput(params.input);
// 3. Perform operation with error handling
try {
const result = processData(sanitized);
// 4. Use appropriate chunk operations
if (bodies.block) {
const resultContext = context.push({ result });
return chunk.render(bodies.block, resultContext);
} else {
return chunk.write(result);
}
} catch (err) {
// 5. Handle errors gracefully
dust.log(`Helper error: ${err.message}`, 'ERROR');
return bodies.error
? chunk.render(bodies.error, context)
: chunk.write('Error processing data');
}
};Install with Tessl CLI
npx tessl i tessl/npm-dustjs-linkedin