Files
skills/docs/foundation/data/templates/src/engine/TemplateEngine.md
2026-05-29 08:20:56 +01:00

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> where TContext 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 reusable CompiledTemplate.
  • Configurable maxDepth to guard against infinite recursion in loop/conditional plugins.
  • throwOnError flag with an onError hook for graceful degradation.
  • MaxDepthError and PluginError propagate without double-wrapping.
  • Protected parseInternal for 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 an Array.prototype.sort — fine for the typical case of ≤20 plugins registered at startup.
  • applyPlugin collects 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.
  • MaxDepthError and PluginError are both explicitly re-thrown without wrapping in applyPlugin. All other exceptions are wrapped in PluginError so callers see a consistent error type with pluginName and match context.
  • parseInternal is protected rather than private so that loop/control plugins can call it for recursive body rendering without a circular dependency on the full engine.