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 output04 / 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 += "&"; break;
case '<': out += "<"; break;
case '>': out += ">"; break;
case '"': out += """; break;
case '\'': out += "'"; 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;
}
}