4.3 KiB
LoopPlugin
Purpose
Iterates over an array in the context object and renders a sub-template once for each element, concatenating the results. Because rendering the loop body requires calling back into the template engine, the plugin accepts the engine's internal parseFunction as a constructor argument rather than implementing its own template resolution. This design keeps the plugin thin and composable while enabling the full plugin pipeline — including nested conditionals, counters, and transforms — to operate inside loop bodies.
Features
{each:items}{item}{/each}— iterate overcontext.items, aliasing each element asitem.{each:items:el}{el.name}{/each}— custom element alias.{_index}— 0-based iteration index available inside the block.{_number}— 1-based iteration number.{_first}—truefor the first iteration,falseotherwise.{_last}—truefor the last iteration,falseotherwise.{_odd}—truewhen index is odd (0-based).{_even}—truewhen index is even (0-based).- Returns
""for empty arrays or non-array values. - Loop bodies are recursively rendered using the injected
parseFunction, so all plugins are available inside the body.
Usage: Basic Example
import { TemplateEngine, createLoopPlugin, createDotNotationPlugin } from "@suaveplan/templates";
const engine = new TemplateEngine();
const pf = (t: string, c: Record<string,unknown>, s: import("@suaveplan/templates").ParseState) =>
(engine as unknown as { parseInternal: typeof pf }).parseInternal(t, c, s);
engine
.use(createLoopPlugin(pf))
.use(createDotNotationPlugin());
engine.parse("{each:names}{item}, {/each}", { names: ["Alice", "Bob", "Carol"] });
// → "Alice, Bob, Carol, "
Usage: Advanced Example
import { TemplateEngine, fullPresetWithLoops } from "@suaveplan/templates";
const engine = new TemplateEngine();
engine.usePreset(
fullPresetWithLoops(
(t, c, s) => (engine as unknown as { parseInternal: Function }).parseInternal(t, c, s)
)
);
const ctx = {
users: [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "editor" },
],
};
engine.parse(
"{each:users:u}{_number}. {u.name}{if:u.role:eq:admin} (admin){/if}\n{/each}",
ctx
);
// → "1. Alice (admin)\n2. Bob\n"
API Reference
createLoopPlugin<TContext>(parseFunction): Plugin<TContext>
Parameters:
parseFunction: (template: string, context: TContext, state: ParseState) => string— the engine's internal synchronous renderer; typicallyengine["parseInternal"].bind(engine).
Returns a Plugin with:
name: "loop"priority: PluginPriority.CRITICAL(95)pattern: /\{each:([a-zA-Z_$][\w.$]*)(?::([a-zA-Z_$][\w]*))?\}([\s\S]*?)\{\/each\}/g
The pattern captures three groups:
- Array path in the context (e.g.
users) - Optional item alias (defaults to
"item") - Loop body template string
transform(match, context, state): Resolves the array path via getNestedValue. For each element, merges the element with the loop meta-variables (_index, _number, _first, _last, _odd, _even) and the item alias into a new context, then calls parseFunction(body, loopCtx, state). Joins all rendered parts and returns the concatenated string. Returns "" for non-arrays or empty arrays.
Implementation Notes
- The
parseFunctionreceives the sameParseStateobject (state) that the outerparse()call created. This means counters inside loop bodies continue from where the outer template left off, which is the expected behaviour for numbered list generation. - Loop meta-variables (
_index,_first, etc.) are injected into a shallow copy of the context object using the spread operator. If the context already contains a key that clashes (e.g. a context field named_index), the loop variable wins. This is intentional and documented. - Runs at
CRITICALpriority so loop blocks are expanded before any transform or variable-resolution plugin touches tokens inside the body, preventing{item}from being replaced as a plain variable before the loop setsitemin context. - The item alias is restricted to a valid JS identifier (
[a-zA-Z_$][\w]*) to prevent ambiguity with path notation. Callers using dot-notation paths as alias names should use a flat alias and access sub-properties explicitly.