import type { ParseState, Plugin, PluginMatch, TemplateContext, } from "../../types/index.js"; import { PluginPriority } from "../../types/index.js"; import { getNestedValue } from "../../utils/nested-value.js"; /** * Loop plugin — iterates over an array in context and renders a sub-template * for each element. * * Syntax: * - `{each:items}{item}{/each}` — default item alias `item` * - `{each:items:el}{el.name}{/each}` — custom alias * * Special loop variables available inside the block: * - `{_index}` — 0-based index * - `{_number}` — 1-based number * - `{_first}` — `true` for the first element * - `{_last}` — `true` for the last element * - `{_odd}` — `true` when index is odd * - `{_even}` — `true` when index is even * * The `parseFunction` parameter is the engine's internal sync-parse so the * plugin can recursively resolve variables inside the loop body. */ export function createLoopPlugin< TContext extends TemplateContext = TemplateContext, >( parseFunction: ( template: string, context: TContext, state: ParseState ) => string ): Plugin { return { name: "loop", priority: PluginPriority.CRITICAL, pattern: /\{each:([a-zA-Z_$][\w.$]*)(?::([a-zA-Z_$][\w]*))?\}([\s\S]*?)\{\/each\}/g, transform( match: PluginMatch, context: TContext, state: ParseState ): string { const arrayPath = match.groups?.[1]; const itemName = match.groups?.[2] ?? "item"; const body = match.groups?.[3]; if (!arrayPath || body === undefined) return match.match; const array = getNestedValue(context, arrayPath); if (!Array.isArray(array) || array.length === 0) return ""; const parts: string[] = []; for (let i = 0; i < array.length; i++) { const isFirst = i === 0; const isLast = i === array.length - 1; const isOdd = i % 2 === 1; const isEven = i % 2 === 0; // Substitute loop meta-variables directly in the body before // recursing. These use the `_` prefix which is reserved and // therefore intentionally skipped by the dot-notation plugin. const renderedBody = body .replace(/\{_index\}/g, String(i)) .replace(/\{_number\}/g, String(i + 1)) .replace(/\{_first\}/g, String(isFirst)) .replace(/\{_last\}/g, String(isLast)) .replace(/\{_odd\}/g, String(isOdd)) .replace(/\{_even\}/g, String(isEven)); const loopCtx = { ...context, [itemName]: array[i], } as TContext; parts.push(parseFunction(renderedBody, loopCtx, state)); } return parts.join(""); }, }; }