Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# 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<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
```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<TContext>(parseFunction): Plugin<TContext>`
**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.