execute method

  1. @override
JinjaValue execute(
  1. Context ctx
)
override

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