# 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 over `context.items`, aliasing each element as `item`. - `{each:items:el}{el.name}{/each}` — custom element alias. - `{_index}` — 0-based iteration index available inside the block. - `{_number}` — 1-based iteration number. - `{_first}` — `true` for the first iteration, `false` otherwise. - `{_last}` — `true` for the last iteration, `false` otherwise. - `{_odd}` — `true` when index is odd (0-based). - `{_even}` — `true` when 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 ```ts import { TemplateEngine, createLoopPlugin, createDotNotationPlugin } from "@suaveplan/templates"; const engine = new TemplateEngine(); const pf = (t: string, c: Record, 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 ```ts 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(parseFunction): Plugin` **Parameters:** - `parseFunction: (template: string, context: TContext, state: ParseState) => string` — the engine's internal synchronous renderer; typically `engine["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: 1. Array path in the context (e.g. `users`) 2. Optional item alias (defaults to `"item"`) 3. 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 `parseFunction` receives the same `ParseState` object (`state`) that the outer `parse()` 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 `CRITICAL` priority 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 sets `item` in 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.