# 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` 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 ```ts 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 ```ts import { TemplateEngine, createLoopPlugin, fullPresetWithLoops } from "@suaveplan/templates"; interface MyCtx { title: string; items: Array<{ label: string; value: number }>; } const engine = new TemplateEngine(); const preset = fullPresetWithLoops( (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): 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): 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[]` 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` Asynchronous rendering path. Awaits `init()`, each plugin `transform()`, and `cleanup()`. Safe to use with async plugins. ### `.compile(template: string): CompiledTemplate` 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.