in packages/jinja/src/runtime.ts [1220:1336]
private evaluateFor(node: For, environment: Environment): StringValue {
// Scope for the for loop
const scope = new Environment(environment);
let test, iterable;
if (node.iterable.type === "SelectExpression") {
const select = node.iterable as SelectExpression;
iterable = this.evaluate(select.lhs, scope);
test = select.test;
} else {
iterable = this.evaluate(node.iterable, scope);
}
if (!(iterable instanceof ArrayValue || iterable instanceof ObjectValue)) {
throw new Error(`Expected iterable or object type in for loop: got ${iterable.type}`);
}
if (iterable instanceof ObjectValue) {
iterable = iterable.keys();
}
const items: Expression[] = [];
const scopeUpdateFunctions: ((scope: Environment) => void)[] = [];
for (let i = 0; i < iterable.value.length; ++i) {
const loopScope = new Environment(scope);
const current = iterable.value[i];
let scopeUpdateFunction;
if (node.loopvar.type === "Identifier") {
scopeUpdateFunction = (scope: Environment) => scope.setVariable((node.loopvar as Identifier).value, current);
} else if (node.loopvar.type === "TupleLiteral") {
const loopvar = node.loopvar as TupleLiteral;
if (current.type !== "ArrayValue") {
throw new Error(`Cannot unpack non-iterable type: ${current.type}`);
}
const c = current as ArrayValue;
// check if too few or many items to unpack
if (loopvar.value.length !== c.value.length) {
throw new Error(`Too ${loopvar.value.length > c.value.length ? "few" : "many"} items to unpack`);
}
scopeUpdateFunction = (scope: Environment) => {
for (let j = 0; j < loopvar.value.length; ++j) {
if (loopvar.value[j].type !== "Identifier") {
throw new Error(`Cannot unpack non-identifier type: ${loopvar.value[j].type}`);
}
scope.setVariable((loopvar.value[j] as Identifier).value, c.value[j]);
}
};
} else {
throw new Error(`Invalid loop variable(s): ${node.loopvar.type}`);
}
if (test) {
scopeUpdateFunction(loopScope);
const testValue = this.evaluate(test, loopScope);
if (!testValue.__bool__().value) {
continue;
}
}
items.push(current);
scopeUpdateFunctions.push(scopeUpdateFunction);
}
let result = "";
let noIteration = true;
for (let i = 0; i < items.length; ++i) {
// Update the loop variable
// TODO: Only create object once, then update value?
const loop = new Map([
["index", new IntegerValue(i + 1)],
["index0", new IntegerValue(i)],
["revindex", new IntegerValue(items.length - i)],
["revindex0", new IntegerValue(items.length - i - 1)],
["first", new BooleanValue(i === 0)],
["last", new BooleanValue(i === items.length - 1)],
["length", new IntegerValue(items.length)],
["previtem", i > 0 ? items[i - 1] : new UndefinedValue()],
["nextitem", i < items.length - 1 ? items[i + 1] : new UndefinedValue()],
] as [string, AnyRuntimeValue][]);
scope.setVariable("loop", new ObjectValue(loop));
// Update scope for this iteration
scopeUpdateFunctions[i](scope);
try {
// Evaluate the body of the for loop
const evaluated = this.evaluateBlock(node.body, scope);
result += evaluated.value;
} catch (err) {
if (err instanceof ContinueControl) {
continue;
}
if (err instanceof BreakControl) {
break;
}
throw err;
}
// At least one iteration took place
noIteration = false;
}
// no iteration took place, so we render the default block
if (noIteration) {
const defaultEvaluated = this.evaluateBlock(node.defaultBlock, scope);
result += defaultEvaluated.value;
}
return new StringValue(result);
}