Files
iios/packages/iios-service/src/templates/template.renderer.ts
T
maaz519 8768af96c1 feat(templates): T1 pure Handlebars renderer
renderTemplate(raw, vars) → {subject, html, text}. HTML body auto-escaped
(customer names into email = XSS risk); subject/text verbatim. Throws on a
missing declared variable — never send a half-rendered receipt. 5 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:21:09 +05:30

47 lines
1.9 KiB
TypeScript

import Handlebars from 'handlebars';
/** The raw, unrendered strings of a template (from the DB row or an inline source). */
export interface RawTemplate {
subject?: string | null;
bodyHtml?: string | null;
bodyText?: string | null;
/** Declared variable names. Every one MUST be supplied at render time (fail loud). */
variables?: string[] | null;
}
export interface RenderedContent {
subject?: string;
html?: string;
text?: string;
}
/** A declared variable was not supplied — we refuse to send a half-rendered "Hi ," message. */
export class MissingTemplateVariableError extends Error {
constructor(public readonly missing: string[]) {
super(`missing required template variable(s): ${missing.join(', ')}`);
this.name = 'MissingTemplateVariableError';
}
}
/**
* Pure render: interpolate `vars` into a template's strings. HTML output is auto-escaped (values
* like a customer name go into an email body — XSS risk); subject and plaintext are NOT escaped.
* No I/O — the caller supplies the raw template. Throws if any declared variable is absent.
*/
export function renderTemplate(tpl: RawTemplate, vars: Record<string, unknown>): RenderedContent {
const declared = tpl.variables ?? [];
const missing = declared.filter((name) => vars[name] === undefined || vars[name] === null);
if (missing.length > 0) throw new MissingTemplateVariableError(missing);
const out: RenderedContent = {};
if (tpl.subject != null) out.subject = compile(tpl.subject, false)(vars);
if (tpl.bodyHtml != null) out.html = compile(tpl.bodyHtml, true)(vars);
if (tpl.bodyText != null) out.text = compile(tpl.bodyText, false)(vars);
return out;
}
/** `escape=true` → HTML-escape interpolated values (for the html body); false → verbatim. */
function compile(src: string, escape: boolean): Handlebars.TemplateDelegate {
return Handlebars.compile(src, { noEscape: !escape, strict: false });
}