private evaluateFor()

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);
	}