4.8 KiB
TemplateEngine
Purpose
The TemplateEngine class is the central orchestrator of @suaveplan/templates. It manages a priority-sorted registry of Plugin objects, applies them sequentially to template strings, and provides both synchronous (parse) and asynchronous (parseAsync) rendering paths. The engine is fully generic over the context type, enabling type-safe template rendering where the compiler validates that required fields are present.
Design philosophy: the engine is deliberately thin — it delegates all transformation logic to plugins and exposes just enough API to register, remove, and invoke them. This keeps the core auditable and makes the plugin contract the unit of composability.
Features
- Generic
TemplateEngine<TContext>whereTContext extends TemplateContext. - Plugin registration via
.use()(strict, no duplicates) and.usePreset()(idempotent). - Priority-sorted plugin execution (higher priority runs first).
- Synchronous
.parse()and asynchronous.parseAsync()rendering. .compile()for pre-binding a template string to produce a reusableCompiledTemplate.- Configurable
maxDepthto guard against infinite recursion in loop/conditional plugins. throwOnErrorflag with anonErrorhook for graceful degradation.MaxDepthErrorandPluginErrorpropagate without double-wrapping.- Protected
parseInternalfor use by sub-plugins (loop, conditional).
Usage: Basic Example
import { TemplateEngine, standardPreset } from "@suaveplan/templates";
const engine = new TemplateEngine().usePreset(standardPreset());
const result = engine.parse("Hello {user.name}!", { user: { name: "World" } });
// → "Hello World!"
Usage: Advanced Example
import { TemplateEngine, createLoopPlugin, fullPresetWithLoops } from "@suaveplan/templates";
interface MyCtx {
title: string;
items: Array<{ label: string; value: number }>;
}
const engine = new TemplateEngine<MyCtx>();
const preset = fullPresetWithLoops<MyCtx>(
(tpl, ctx, state) => (engine as any).parseInternal(tpl, ctx, state)
);
engine.usePreset(preset);
const result = engine.parse(
"{title}:\n{each:items} - {item.label}: {item.value}\n{/each}",
{ title: "Report", items: [{ label: "A", value: 1 }, { label: "B", value: 2 }] }
);
// → "Report:\n - A: 1\n - B: 2\n"
API Reference
constructor(options?: TemplateEngineOptions)
| Option | Type | Default | Description |
|---|---|---|---|
maxDepth |
number |
10 |
Maximum recursive nesting depth |
throwOnError |
boolean |
true |
Re-throw errors vs. call onError |
enableCache |
boolean |
true |
Cache sub-template results |
onError |
function |
no-op | Called with (error, template, context) when throwOnError is false |
.use(plugin: Plugin<TContext>): this
Registers a plugin. Throws TemplateError(PLUGIN_CONFLICT) if a plugin with the same name is already registered. Triggers a priority re-sort.
.usePreset(preset: PluginPreset<TContext>): this
Registers all plugins in the preset. Plugins whose name is already registered are silently skipped. Triggers a priority re-sort.
.remove(name: string): this
Removes a plugin by name. No-op when the name is not found.
.getPlugins(): readonly Plugin<TContext>[]
Returns a snapshot of the current plugin list, sorted by priority descending.
.parse(template, context, options?): string
Synchronously renders template against context. Throws if any plugin's init() or cleanup() returns a Promise (use parseAsync() instead). When throwOnError is false, errors are forwarded to onError and the original template string is returned.
.parseAsync(template, context, options?): Promise<string>
Asynchronous rendering path. Awaits init(), each plugin transform(), and cleanup(). Safe to use with async plugins.
.compile(template: string): CompiledTemplate<TContext>
Returns { source, parse(context), parseAsync(context) }. Suitable for templates rendered many times with different contexts.
Implementation Notes
- Plugins are stored in a single sorted array. Each
.use()or.usePreset()triggers anArray.prototype.sort— fine for the typical case of ≤20 plugins registered at startup. applyPlugincollects all matches first, then applies replacements in reverse index order. This prevents offset drift when a replacement is shorter or longer than the matched text.MaxDepthErrorandPluginErrorare both explicitly re-thrown without wrapping inapplyPlugin. All other exceptions are wrapped inPluginErrorso callers see a consistent error type withpluginNameandmatchcontext.parseInternalisprotectedrather thanprivateso that loop/control plugins can call it for recursive body rendering without a circular dependency on the full engine.