89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
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<TContext> {
|
|
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("");
|
|
},
|
|
};
|
|
}
|