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.
    // Keys with the same string form are iterated once, as in asMap.
    final keys = <String, JinjaValue>{
      for (final k in iterableVal.items.keys) k.toString(): k,
    };
    items = [
      for (final MapEntry(:key, :value) in keys.entries)
        value is JinjaStringValue && hasRawInput(value.value)
            ? value
            : JinjaStringValue.fromString(key),
    ];
  } else if (iterableVal is JinjaList) {
    items = iterableVal.items;
  } else if (iterableVal is JinjaTuple) {
    items = iterableVal.items;
  } else if (iterableVal is JinjaStringValue) {
    items = charsOf(iterableVal.value);
    deriveRendered(JinjaList(items), [iterableVal]);
  }

  // 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
  final output = <JinjaString>[];
  // 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) output.add(result.value);
      iterated = true;
    } on ContinueSignal catch (signal) {
      output.addAll(signal.output);
    } on BreakSignal catch (signal) {
      output.addAll(signal.output);
      break;
    }
  }

  if (!iterated) {
    try {
      final result = execStatements(defaultBlock, ctx);
      if (result is JinjaStringValue) output.add(result.value);
    } 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.output.insertAll(0, output);
      rethrow;
    }
  }
  return JinjaStringValue(_joinOutput(output));
}