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,88 @@
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("");
},
};
}