execute method
Executes the statement within the given ctx and returns the result as a JinjaValue.
Implementation
@override
JinjaValue execute(Context ctx) {
// New scope for loop
// Evaluate iterable
// Handle SelectExpression (filtering) if present in iterable
Expression iterExpr = iterable;
Expression? testExpr;
if (iterable is SelectExpression) {
final sel = iterable as SelectExpression;
iterExpr = sel.lhs;
testExpr = sel.test;
}
JinjaValue iterableVal = iterExpr.execute(ctx);
if (iterableVal.isUndefined) {
iterableVal = const JinjaList([]);
}
if (!iterableVal.isList && !iterableVal.isMap && !iterableVal.isString) {
final got = iterableVal is JinjaFunction
? "${iterableVal.typeName} '${iterableVal.name}'"
: iterableVal.typeName;
throw Exception('Expected iterable in for loop: got $got');
}
List<JinjaValue> items = [];
if (iterableVal is JinjaMap) {
// Loop over keys
// Sort keys for deterministic output if needed, though Jinja2 doesn't strictly guarantee order, usually tests expect sorted keys or we should sort.
// Standard Jinja2 iterates keys.
// llama.cpp tests seem to expect keys.
final keys = iterableVal.asMap.keys.toList();
// keys.sort(); // Optional: sort keys? Dinja existing tests expect 'a,b'. Maps in Dart preserve insertion order? LinkedHashMap does.
// If the map came from JSON/Literal, it's LinkedHashMap.
items = keys.map((k) => JinjaStringValue.fromString(k)).toList();
} else if (iterableVal is JinjaList) {
items = iterableVal.items;
} else if (iterableVal is JinjaTuple) {
items = iterableVal.items;
} else if (iterableVal is JinjaStringValue) {
// Iterate chars
items = iterableVal.value.parts
.map((p) => p.val)
.expand((s) => s.split(''))
.map((c) => JinjaStringValue.fromString(c))
.toList();
}
// Filter items first if testExpr
if (testExpr != null) {
List<JinjaValue> filtered = [];
for (final item in items) {
// Create temp context to evaluate test
// We need to bind loop var to item
final tempCtx = ctx.derive();
_bindLoopVar(tempCtx, loopVar, item);
if (testExpr.execute(tempCtx).asBool) {
filtered.add(item);
}
}
items = filtered;
}
// Loop
List<JinjaStringPart> parts = [];
// As in Jinja2 and llama.cpp, the else block runs unless a pass finishes
// without `break` or `continue`.
var iterated = false;
final loopCtx = ctx.derive(); // Scope for loop execution
for (int i = 0; i < items.length; i++) {
final item = items[i];
// Loop object
final loopObj = {
'index': JinjaInteger(i + 1),
'index0': JinjaInteger(i),
'revindex': JinjaInteger(items.length - i),
'revindex0': JinjaInteger(items.length - i - 1),
'first': JinjaBoolean(i == 0),
'last': JinjaBoolean(i == items.length - 1),
'length': JinjaInteger(items.length),
'previtem': i > 0 ? items[i - 1] : const JinjaUndefined(),
'nextitem': i < items.length - 1
? items[i + 1]
: const JinjaUndefined(),
};
loopCtx.set('loop', JinjaLoopContext(loopObj));
_bindLoopVar(loopCtx, loopVar, item);
try {
final result = execStatements(body, loopCtx);
if (result is JinjaStringValue) parts.addAll(result.value.parts);
iterated = true;
} on ContinueSignal catch (signal) {
parts.addAll(signal.parts);
} on BreakSignal catch (signal) {
parts.addAll(signal.parts);
break;
}
}
if (!iterated) {
try {
final result = execStatements(defaultBlock, ctx);
if (result is JinjaStringValue) parts.addAll(result.value.parts);
} on LoopSignal catch (signal) {
// A `break` or `continue` in the else block belongs to an enclosing
// loop and keeps the output of this one, as in Jinja2.
signal.parts.insertAll(0, parts);
rethrow;
}
}
return JinjaStringValue(JinjaString(parts, isSafe: true));
}