first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user