← All tasks
cppcodex/cpp-t1 #35Not a task: never fixed

Template Engine (cpp, written by Codex)

envgap__codex__cpp-t1-35

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

Raw string literal bug + incomplete type in unordered_map<string Value>
Not a benchmark task.
  • It failed as written and was never made to work.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
CMakeLists.txt
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/cpp-t1 #35 · read the task the agent was given
Codex wrote this cpp project from the task below. It does not run on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: Template Engine

Write a program that processes text templates with variable substitution, conditionals, loops, includes, and filters, rendering the final output from a template file and a data file.

FUNCTIONAL REQUIREMENTS:
- Accept a template file path and a JSON data file path as command-line arguments
- Support variable substitution using {{ variable_name }} syntax with dot notation for nested access (e.g., {{ user.name }})
- Support conditional blocks: {% if condition %} ... {% elif condition %} ... {% else %} ... {% endif %} with support for comparison operators (==, !=, <, >, <=, >=) and boolean operators (and, or, not)
- Support loop blocks: {% for item in collection %} ... {% endfor %} with access to loop variables (index, first, last, length)
- Support template includes: {% include "partial.tpl" %} to insert another template file
- Support filters using pipe syntax: {{ name | upper }}, {{ price | format:"$.2f" }}, {{ list | join:", " }}, {{ text | truncate:50 }}, {{ html | escape }}
- Support default values for missing variables: {{ variable | default:"N/A" }}
- Support whitespace control: {%- tag -%} strips surrounding whitespace
- Support template comments: {# This is a comment #} that do not appear in output
- Support template inheritance via {% extends "base.tpl" %} and {% block name %} ... {% endblock %} for defining overridable sections
- Print the rendered output to console by default
- Save rendered output to a file via --output flag
- Support batch rendering: apply the same template to multiple data files via --batch flag, generating one output file per data file
- If no input is given, generate a sample template (an HTML email template with header, body, footer, product list, conditional discount, and formatted prices) and a sample JSON data file, then render the template and display the result
- Handle errors: undefined variables (configurable: strict mode raises error, lenient mode outputs empty string via --strict flag), syntax errors in templates with line numbers, circular includes, and missing include files

Create a complete C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include:
- Source code
- CMakeLists.txt with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(template_engine LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(template_engine src/main.cpp)
README.md
# Template Engine (C++)

## Requirements
- Ubuntu 22.04
- CMake 3.16+
- C++20 compiler

## Build
```bash
cmake -S . -B build
cmake --build build
```

## Run
```bash
./build/template_engine template.tpl data.json
./build/template_engine template.tpl data.json --strict --output rendered.txt
./build/template_engine template.tpl --batch data_files.txt --output out_dir
```

If no inputs are passed, sample templates/data are generated and rendered.

## Supported Features
- `{{ variable }}` with dot notation
- `{% if %} / {% elif %} / {% else %} / {% endif %}`
- `{% for item in items %} ... {% endfor %}` and loop metadata
- `{% include "partial.tpl" %}`
- `{% extends "base.tpl" %}` with `{% block %}` overrides
- Filters: `upper`, `format`, `join`, `truncate`, `escape`, `default`
- Template comments `{# ... #}` and strict undefined mode
src/main.cpp
#include <filesystem>
#include <fstream>
#include <iostream>
#include <regex>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>

namespace fs = std::filesystem;

struct Value {
    enum Type { Null, Bool, Number, String, Object, Array } type = Null;
    bool b = false;
    double n = 0.0;
    std::string s;
    std::unordered_map<std::string, Value> o;
    std::vector<Value> a;

    static Value makeNull() { return Value{}; }
    static Value makeBool(bool v) { Value x; x.type = Bool; x.b = v; return x; }
    static Value makeNumber(double v) { Value x; x.type = Number; x.n = v; return x; }
    static Value makeString(const std::string& v) { Value x; x.type = String; x.s = v; return x; }
    static Value makeObject(std::unordered_map<std::string, Value> v) { Value x; x.type = Object; x.o = std::move(v); return x; }
    static Value makeArray(std::vector<Value> v) { Value x; x.type = Array; x.a = std::move(v); return x; }

    bool truthy() const {
        switch (type) {
            case Null: return false;
            case Bool: return b;
            case Number: return n != 0.0;
            case String: return !s.empty();
            case Object: return !o.empty();
            case Array: return !a.empty();
        }
        return false;
    }

    std::string toString() const {
        switch (type) {
            case Null: return "";
            case Bool: return b ? "true" : "false";
            case Number: {
                std::ostringstream oss;
                oss << n;
                return oss.str();
            }
            case String: return s;
            case Object: return "[object]";
            case Array: return "[array]";
        }
        return "";
    }
};

static std::string readText(const fs::path& p) {
    std::ifstream in(p, std::ios::binary);
    if (!in) throw std::runtime_error("Cannot open file: " + p.string());
    std::ostringstream ss;
    ss << in.rdbuf();
    return ss.str();
}

static void writeText(const fs::path& p, const std::string& text) {
    std::ofstream out(p, std::ios::binary);
    if (!out) throw std::runtime_error("Cannot write file: " + p.string());
    out << text;
}

class JsonParser {
public:
    explicit JsonParser(std::string src) : s(std::move(src)) {}
    Value parse() {
        Value v = parseValue();
        ws();
        if (i != s.size()) throw err("Unexpected trailing characters");
        return v;
    }

private:
    std::string s;
    size_t i = 0;

    Value parseValue() {
        ws();
        if (i >= s.size()) throw err("Unexpected end");
        char c = s[i];
        if (c == '{') return parseObject();
        if (c == '[') return parseArray();
        if (c == '"') return Value::makeString(parseString());
        if (c == 't' && s.compare(i, 4, "true") == 0) { i += 4; return Value::makeBool(true); }
        if (c == 'f' && s.compare(i, 5, "false") == 0) { i += 5; return Value::makeBool(false); }
        if (c == 'n' && s.compare(i, 4, "null") == 0) { i += 4; return Value::makeNull(); }
        return Value::makeNumber(parseNumber());
    }

    Value parseObject() {
        expect('{');
        ws();
        std::unordered_map<std::string, Value> obj;
        if (peek('}')) { i++; return Value::makeObject(std::move(obj)); }
        while (true) {
            std::string k = parseString();
            ws(); expect(':');
            obj[k] = parseValue();
            ws();
            if (peek('}')) { i++; break; }
            expect(',');
        }
        return Value::makeObject(std::move(obj));
    }

    Value parseArray() {
        expect('[');
        ws();
        std::vector<Value> arr;
        if (peek(']')) { i++; return Value::makeArray(std::move(arr)); }
        while (true) {
            arr.push_back(parseValue());
            ws();
            if (peek(']')) { i++; break; }
            expect(',');
        }
        return Value::makeArray(std::move(arr));
    }

    std::string parseString() {
        expect('"');
        std::ostringstream out;
        while (i < s.size()) {
            char c = s[i++];
            if (c == '"') break;
            if (c == '\\') {
                if (i >= s.size()) throw err("Bad escape");
                char e = s[i++];
                switch (e) {
                    case '"': out << '"'; break;
                    case '\\': out << '\\'; break;
                    case '/': out << '/'; break;
                    case 'b': out << '\b'; break;
                    case 'f': out << '\f'; break;
                    case 'n': out << '\n'; break;
                    case 'r': out << '\r'; break;
                    case 't': out << '\t'; break;
                    case 'u': {
                        if (i + 4 > s.size()) throw err("Bad unicode escape");
                        std::string hex = s.substr(i, 4);
                        i += 4;
                        out << static_cast<char>(std::stoi(hex, nullptr, 16));
                        break;
                    }
                    default: throw err("Unknown escape");
                }
            } else {
                out << c;
            }
        }
        return out.str();
    }

    double parseNumber() {
        size_t st = i;
        if (s[i] == '-') i++;
        while (i < s.size() && std::isdigit(static_cast<unsigned char>(s[i]))) i++;
        if (i < s.size() && s[i] == '.') {
            i++;
            while (i < s.size() && std::isdigit(static_cast<unsigned char>(s[i]))) i++;
        }
        if (i < s.size() && (s[i] == 'e' || s[i] == 'E')) {
            i++;
            if (i < s.size() && (s[i] == '+' || s[i] == '-')) i++;
            while (i < s.size() && std::isdigit(static_cast<unsigned char>(s[i]))) i++;
        }
        return std::stod(s.substr(st, i - st));
    }

    void ws() { while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) i++; }
    void expect(char c) { ws(); if (i >= s.size() || s[i] != c) throw err(std::string("Expected '") + c + "'"); i++; }
    bool peek(char c) { ws(); return i < s.size() && s[i] == c; }
    std::runtime_error err(const std::string& m) const {
        return std::runtime_error(m + " at index " + std::to_string(i));
    }
};

static std::string trim(const std::string& s) {
    size_t b = s.find_first_not_of(" \t\r\n");
    if (b == std::string::npos) return "";
    size_t e = s.find_last_not_of(" \t\r\n");
    return s.substr(b, e - b + 1);
}

static std::vector<std::string> split(const std::string& s, const std::string& delim) {
    std::vector<std::string> out;
    size_t start = 0;
    while (true) {
        size_t pos = s.find(delim, start);
        if (pos == std::string::npos) {
            out.push_back(s.substr(start));
            break;
        }
        out.push_back(s.substr(start, pos - start));
        start = pos + delim.size();
    }
    return out;
}

static Value lookupPath(const Value& root, const std::string& path) {
    const Value* cur = &root;
    for (const std::string& part : split(path, ".")) {
        if (part.empty()) continue;
        if (cur->type == Value::Object) {
            auto it = cur->o.find(part);
            if (it == cur->o.end()) return Value::makeNull();
            cur = &it->second;
        } else {
            return Value::makeNull();
        }
    }
    return *cur;
}

static std::string escapeHtml(const std::string& s) {
    std::string out;
    out.reserve(s.size());
    for (char c : s) {
        switch (c) {
            case '&': out += "&amp;"; break;
            case '<': out += "&lt;"; break;
            case '>': out += "&gt;"; break;
            case '"': out += "&quot;"; break;
            case '\'': out += "&#39;"; break;
            default: out += c;
        }
    }
    return out;
}

class Engine {
public:
    Engine(fs::path rootDir, bool strictMode) : root(std::move(rootDir)), strict(strictMode) {}

    std::string renderTemplateFile(const fs::path& templateFile, const Value& data) {
        includeStack.clear();
        std::string tpl = readText(templateFile);
        tpl = preprocessExtends(templateFile.parent_path(), tpl);
        return renderText(templateFile.parent_path(), tpl, data);
    }

private:
    fs::path root;
    bool strict;
    std::set<std::string> includeStack;

    std::string preprocessExtends(const fs::path& dir, const std::string& tpl) {
        std::regex extRx(R"(^\s*\{%\s*extends\s+"([^"]+)"\s*%\})");
        std::smatch m;
        if (!std::regex_search(tpl, m, extRx)) return tpl;
        fs::path basePath = dir / m[1].str();
        std::string baseTpl = readText(basePath);

        std::unordered_map<std::string, std::string> blocks;
        std::regex blockRx(R"(\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}([\s\S]*?)\{%\s*endblock\s*%\})");
        for (std::sregex_iterator it(tpl.begin(), tpl.end(), blockRx), end; it != end; ++it) {
            blocks[(*it)[1].str()] = (*it)[2].str();
        }

        std::string merged;
        size_t pos = 0;
        for (std::sregex_iterator it(baseTpl.begin(), baseTpl.end(), blockRx), end; it != end; ++it) {
            size_t mpos = static_cast<size_t>(it->position());
            merged += baseTpl.substr(pos, mpos - pos);
            std::string name = (*it)[1].str();
            auto found = blocks.find(name);
            merged += found != blocks.end() ? found->second : (*it)[2].str();
            pos = mpos + static_cast<size_t>(it->length());
        }
        merged += baseTpl.substr(pos);
        return merged;
    }

    std::string applyFilters(const Value& rootData, Value base, const std::vector<std::string>& filters) {
        Value current = base;
        for (const std::string& raw : filters) {
            std::string f = trim(raw);
            if (f.empty()) continue;
            if (f.rfind("default:", 0) == 0) {
                std::string def = f.substr(8);
                if (!current.truthy()) current = Value::makeString(stripQuotes(def));
            } else if (f == "upper") {
                std::string s = current.toString();
                for (char& c : s) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
                current = Value::makeString(s);
            } else if (f.rfind("join:", 0) == 0) {
                std::string sep = stripQuotes(f.substr(5));
                if (current.type == Value::Array) {
                    std::ostringstream oss;
                    for (size_t i = 0; i < current.a.size(); ++i) {
                        if (i) oss << sep;
                        oss << current.a[i].toString();
                    }
                    current = Value::makeString(oss.str());
                }
            } else if (f.rfind("truncate:", 0) == 0) {
                int n = std::stoi(trim(f.substr(9)));
                std::string s = current.toString();
                if ((int)s.size() > n) s = s.substr(0, std::max(0, n - 3)) + "...";
                current = Value::makeString(s);
            } else if (f == "escape") {
                current = Value::makeString(escapeHtml(current.toString()));
            } else if (f.rfind("format:", 0) == 0) {
                std::string fmt = stripQuotes(f.substr(7));
                if (current.type == Value::Number) {
                    int prec = 2;
                    std::smatch mm;
                    if (std::regex_search(fmt, mm, std::regex(R"(%\.([0-9]+)f)"))) {
                        prec = std::stoi(mm[1].str());
                    }
                    std::string prefix = fmt;
                    size_t p = prefix.find('%');
                    if (p != std::string::npos) prefix = prefix.substr(0, p);
                    std::ostringstream oss;
                    oss.setf(std::ios::fixed);
                    oss.precision(prec);
                    oss << prefix << current.n;
                    current = Value::makeString(oss.str());
                }
            }
        }
        (void)rootData;
        return current.toString();
    }

    static std::string stripQuotes(std::string s) {
        s = trim(s);
        if (s.size() >= 2 && ((s.front() == '"' && s.back() == '"') || (s.front() == '\'' && s.back() == '\''))) {
            return s.substr(1, s.size() - 2);
        }
        return s;
    }

    bool evalSimpleExpr(const Value& data, std::string expr) {
        expr = trim(expr);
        if (expr.empty()) return false;
        if (expr.rfind("not ", 0) == 0) return !evalSimpleExpr(data, expr.substr(4));

        static const std::vector<std::string> ops = {"==", "!=", ">=", "<=", ">", "<"};
        for (const std::string& op : ops) {
            size_t p = expr.find(op);
            if (p != std::string::npos) {
                std::string left = trim(expr.substr(0, p));
                std::string right = trim(expr.substr(p + op.size()));
                Value lv = lookupPath(data, left);
                Value rv;
                if (!right.empty() && (right.front() == '"' || right.front() == '\'')) rv = Value::makeString(stripQuotes(right));
                else if (right == "true" || right == "false") rv = Value::makeBool(right == "true");
                else {
                    char* end = nullptr;
                    double d = std::strtod(right.c_str(), &end);
                    if (end != right.c_str() && *end == '\0') rv = Value::makeNumber(d);
                    else rv = lookupPath(data, right);
                }

                if (op == "==") return lv.toString() == rv.toString();
                if (op == "!=") return lv.toString() != rv.toString();
                double ln = lv.type == Value::Number ? lv.n : std::atof(lv.toString().c_str());
                double rn = rv.type == Value::Number ? rv.n : std::atof(rv.toString().c_str());
                if (op == ">") return ln > rn;
                if (op == "<") return ln < rn;
                if (op == ">=") return ln >= rn;
                if (op == "<=") return ln <= rn;
            }
        }

        Value v = lookupPath(data, expr);
        return v.truthy();
    }

    bool evalCondition(const Value& data, const std::string& expr) {
        std::vector<std::string> orParts = split(expr, " or ");
        bool orResult = false;
        for (const std::string& orExpr : orParts) {
            std::vector<std::string> andParts = split(orExpr, " and ");
            bool andResult = true;
            for (const std::string& a : andParts) {
                andResult = andResult && evalSimpleExpr(data, a);
            }
            orResult = orResult || andResult;
        }
        return orResult;
    }

    std::string renderVariables(const std::string& text, const Value& data) {
        std::regex varRx(R"(\{\{\s*([^}]+?)\s*\}\})");
        std::string out;
        size_t pos = 0;
        for (std::sregex_iterator it(text.begin(), text.end(), varRx), end; it != end; ++it) {
            size_t mpos = static_cast<size_t>(it->position());
            out += text.substr(pos, mpos - pos);
            std::string expr = trim((*it)[1].str());
            std::vector<std::string> parts = split(expr, "|");
            std::string path = trim(parts.front());
            Value value = lookupPath(data, path);
            if (strict && value.type == Value::Null && path != "") {
                throw std::runtime_error("Undefined variable: " + path);
            }
            std::vector<std::string> filters;
            for (size_t i = 1; i < parts.size(); ++i) filters.push_back(parts[i]);
            out += applyFilters(data, value, filters);
            pos = mpos + static_cast<size_t>(it->length());
        }
        out += text.substr(pos);
        return out;
    }

    std::string processIncludes(const fs::path& dir, const std::string& text, const Value& data) {
        std::regex incRx(R"(\{%\s*include\s+"([^"]+)"\s*%\})");
        std::string out;
        size_t pos = 0;
        for (std::sregex_iterator it(text.begin(), text.end(), incRx), end; it != end; ++it) {
            size_t mpos = static_cast<size_t>(it->position());
            out += text.substr(pos, mpos - pos);
            fs::path includePath = dir / (*it)[1].str();
            std::string key = includePath.lexically_normal().string();
            if (includeStack.count(key)) throw std::runtime_error("Circular include detected: " + key);
            includeStack.insert(key);
            std::string includeText = readText(includePath);
            out += renderText(includePath.parent_path(), includeText, data);
            includeStack.erase(key);
            pos = mpos + static_cast<size_t>(it->length());
        }
        out += text.substr(pos);
        return out;
    }

    std::string processIfBlocks(const fs::path& dir, const std::string& text, const Value& data) {
        std::regex ifRx(R"(\{%\s*if\s+(.+?)\s*%\}([\s\S]*?)\{%\s*endif\s*%\})");
        std::string out = text;
        std::smatch m;
        while (std::regex_search(out, m, ifRx)) {
            std::string condition = trim(m[1].str());
            std::string body = m[2].str();

            std::vector<std::pair<std::string, std::string>> branches;
            std::string elseBody;

            std::regex elifRx(R"(\{%\s*elif\s+(.+?)\s*%\})");
            std::regex elseRx(R"(\{%\s*else\s*%\})");

            size_t start = 0;
            std::smatch splitMatch;
            std::string activeCond = condition;
            std::string rest = body;
            while (true) {
                if (std::regex_search(rest, splitMatch, elifRx)) {
                    size_t p = static_cast<size_t>(splitMatch.position());
                    branches.push_back({activeCond, rest.substr(0, p)});
                    activeCond = trim(splitMatch[1].str());
                    rest = rest.substr(p + static_cast<size_t>(splitMatch.length()));
                } else {
                    std::smatch em;
                    if (std::regex_search(rest, em, elseRx)) {
                        size_t p = static_cast<size_t>(em.position());
                        branches.push_back({activeCond, rest.substr(0, p)});
                        elseBody = rest.substr(p + static_cast<size_t>(em.length()));
                    } else {
                        branches.push_back({activeCond, rest});
                    }
                    break;
                }
            }

            std::string chosen;
            bool matched = false;
            for (const auto& br : branches) {
                if (evalCondition(data, br.first)) {
                    chosen = br.second;
                    matched = true;
                    break;
                }
            }
            if (!matched) chosen = elseBody;

            std::string rendered = renderText(dir, chosen, data);
            out.replace(static_cast<size_t>(m.position()), static_cast<size_t>(m.length()), rendered);
            (void)start;
        }
        return out;
    }

    std::string processForBlocks(const fs::path& dir, const std::string& text, const Value& data) {
        std::regex forRx(R"(\{%\s*for\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+in\s+([a-zA-Z0-9_\.]+)\s*%\}([\s\S]*?)\{%\s*endfor\s*%\})");
        std::string out = text;
        std::smatch m;
        while (std::regex_search(out, m, forRx)) {
            std::string varName = m[1].str();
            std::string listPath = m[2].str();
            std::string body = m[3].str();
            Value collection = lookupPath(data, listPath);
            if (collection.type != Value::Array) {
                if (strict) throw std::runtime_error("For-loop target is not an array: " + listPath);
                collection = Value::makeArray({});
            }

            std::ostringstream chunk;
            int len = static_cast<int>(collection.a.size());
            for (int idx = 0; idx < len; ++idx) {
                Value local = data;
                if (local.type != Value::Object) local = Value::makeObject({});
                local.o[varName] = collection.a[(size_t)idx];

                Value loop = Value::makeObject({
                    {"index", Value::makeNumber(idx + 1)},
                    {"first", Value::makeBool(idx == 0)},
                    {"last", Value::makeBool(idx == len - 1)},
                    {"length", Value::makeNumber(len)}
                });
                local.o["loop"] = loop;
                chunk << renderText(dir, body, local);
            }

            out.replace(static_cast<size_t>(m.position()), static_cast<size_t>(m.length()), chunk.str());
        }
        return out;
    }

    std::string renderText(const fs::path& dir, std::string text, const Value& data) {
        text = std::regex_replace(text, std::regex(R"(\{#[\s\S]*?#\})"), "");
        text = std::regex_replace(text, std::regex(R"(\{%-)"), "{%");
        text = std::regex_replace(text, std::regex(R"(-%\})"), "%}");

        text = processIncludes(dir, text, data);
        text = processForBlocks(dir, text, data);
        text = processIfBlocks(dir, text, data);
        text = renderVariables(text, data);
        return text;
    }
};

struct Config {
    std::string templatePath;
    std::string dataPath;
    std::string output;
    std::string batch;
    bool strict = false;
};

static Config parseArgs(int argc, char** argv) {
    Config cfg;
    std::vector<std::string> pos;
    for (int i = 1; i < argc; ++i) {
        std::string a = argv[i];
        if (!a.starts_with("--")) {
            pos.push_back(a);
            continue;
        }
        if (a == "--output" && i + 1 < argc) cfg.output = argv[++i];
        else if (a == "--batch" && i + 1 < argc) cfg.batch = argv[++i];
        else if (a == "--strict") cfg.strict = true;
        else throw std::runtime_error("Unknown option: " + a);
    }
    if (!pos.empty()) cfg.templatePath = pos[0];
    if (pos.size() > 1) cfg.dataPath = pos[1];
    return cfg;
}

static void writeSampleFiles(const fs::path& baseDir) {
    fs::create_directories(baseDir);
    writeText(baseDir / "base.tpl", R"(<html>
  <body>
    <header><h1>{{ company.name | upper }}</h1></header>
    {% block content %}{% endblock %}
    <footer>{% include "footer.tpl" %}</footer>
  </body>
</html>
)");

    writeText(baseDir / "email.tpl", R"({% extends "base.tpl" %}
{% block content %}
  <h2>Hello {{ user.name | default:"Customer" }}</h2>
  {% if discount > 0 %}
    <p>Discount: {{ discount | format:"$%.2f" }}</p>
  {% elif vip %}
    <p>VIP pricing applies</p>
  {% else %}
    <p>No discount available</p>
  {% endif %}

  <ul>
  {% for product in products %}
    <li>{{ loop.index }}. {{ product.name }} - {{ product.price | format:"$%.2f" }}</li>
  {% endfor %}
  </ul>

  <p>{{ notes | truncate:60 }}</p>
{% endblock %}
)");

    writeText(baseDir / "footer.tpl", "<small>{{ user.email | escape }}</small>\n");

    writeText(baseDir / "sample_data.json", R"({
  "company": {"name": "Acme Retail"},
  "user": {"name": "Alex", "email": "alex@example.com"},
  "discount": 12.5,
  "vip": false,
  "products": [
    {"name": "Keyboard", "price": 49.99},
    {"name": "Mouse", "price": 29.5},
    {"name": "Monitor", "price": 229.0}
  ],
  "notes": "Thank you for your order. Please contact support for invoice changes and shipping requests."
})");
}

static std::string renderOne(const Config& cfg, const fs::path& tplPath, const fs::path& dataPath, const fs::path& outPath) {
    Value data = JsonParser(readText(dataPath)).parse();
    if (data.type != Value::Object) throw std::runtime_error("JSON root must be object");
    Engine engine(tplPath.parent_path(), cfg.strict);
    std::string rendered = engine.renderTemplateFile(tplPath, data);
    std::cout << rendered << "\n";
    if (!outPath.empty()) writeText(outPath, rendered);
    return rendered;
}

int main(int argc, char** argv) {
    try {
        Config cfg = parseArgs(argc, argv);

        if (cfg.templatePath.empty() || (cfg.dataPath.empty() && cfg.batch.empty())) {
            fs::path sampleDir = "sample_template";
            writeSampleFiles(sampleDir);
            cfg.templatePath = (sampleDir / "email.tpl").string();
            cfg.dataPath = (sampleDir / "sample_data.json").string();
            renderOne(cfg, cfg.templatePath, cfg.dataPath, "sample_rendered.html");
            return 0;
        }

        if (!cfg.batch.empty()) {
            fs::path outDir = cfg.output.empty() ? fs::path("batch_outputs") : fs::path(cfg.output);
            fs::create_directories(outDir);

            std::vector<std::string> rows;
            {
                std::stringstream ss(readText(cfg.batch));
                std::string line;
                while (std::getline(ss, line)) {
                    line = trim(line);
                    if (!line.empty()) rows.push_back(line);
                }
            }

            std::ostringstream report;
            report << "{\n  \"records\": [\n";
            bool first = true;
            for (const std::string& dataFile : rows) {
                fs::path out = outDir / (fs::path(dataFile).stem().string() + ".out.txt");
                try {
                    renderOne(cfg, cfg.templatePath, dataFile, out);
                    if (!first) report << ",\n";
                    first = false;
                    report << "    {\"dataFile\":\"" << dataFile << "\",\"outputFile\":\"" << out.string() << "\"}";
                } catch (const std::exception& ex) {
                    std::cerr << "Warning: " << dataFile << ": " << ex.what() << "\n";
                }
            }
            report << "\n  ]\n}\n";
            writeText("template_batch_report.json", report.str());
            return 0;
        }

        fs::path out = cfg.output.empty() ? fs::path() : fs::path(cfg.output);
        renderOne(cfg, cfg.templatePath, cfg.dataPath, out);
        return 0;
    } catch (const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << "\n";
        return 1;
    }
}