Initial Commit
This commit is contained in:
@@ -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("");
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user