mirror of
https://github.com/pantor/inja.git
synced 2026-03-22 08:52:47 +00:00
612 lines
18 KiB
C++
612 lines
18 KiB
C++
// Copyright (c) 2019 Pantor. All rights reserved.
|
|
|
|
#ifndef INCLUDE_INJA_RENDERER_HPP_
|
|
#define INCLUDE_INJA_RENDERER_HPP_
|
|
|
|
#include <algorithm>
|
|
#include <numeric>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include "config.hpp"
|
|
#include "exceptions.hpp"
|
|
#include "node.hpp"
|
|
#include "template.hpp"
|
|
#include "utils.hpp"
|
|
|
|
namespace inja {
|
|
|
|
inline nonstd::string_view convert_dot_to_json_pointer(nonstd::string_view dot, std::string &out) {
|
|
out.clear();
|
|
do {
|
|
nonstd::string_view part;
|
|
std::tie(part, dot) = string_view::split(dot, '.');
|
|
out.push_back('/');
|
|
out.append(part.begin(), part.end());
|
|
} while (!dot.empty());
|
|
return nonstd::string_view(out.data(), out.size());
|
|
}
|
|
|
|
/*!
|
|
* \brief Class for rendering a Template with data.
|
|
*/
|
|
class Renderer {
|
|
std::vector<const json *> &get_args(const Node &node) {
|
|
m_tmp_args.clear();
|
|
|
|
bool has_imm = ((node.flags & Node::Flag::ValueMask) != Node::Flag::ValuePop);
|
|
|
|
// get args from stack
|
|
unsigned int pop_args = node.args;
|
|
if (has_imm) {
|
|
pop_args -= 1;
|
|
}
|
|
|
|
for (auto i = std::prev(m_stack.end(), pop_args); i != m_stack.end(); i++) {
|
|
m_tmp_args.push_back(&(*i));
|
|
}
|
|
|
|
// get immediate arg
|
|
if (has_imm) {
|
|
m_tmp_args.push_back(get_imm(node));
|
|
}
|
|
|
|
return m_tmp_args;
|
|
}
|
|
|
|
void pop_args(const Node &node) {
|
|
unsigned int pop_args = node.args;
|
|
if ((node.flags & Node::Flag::ValueMask) != Node::Flag::ValuePop) {
|
|
pop_args -= 1;
|
|
}
|
|
for (unsigned int i = 0; i < pop_args; ++i) {
|
|
m_stack.pop_back();
|
|
}
|
|
}
|
|
|
|
const json *get_imm(const Node &node) {
|
|
std::string ptr_buffer;
|
|
nonstd::string_view ptr;
|
|
switch (node.flags & Node::Flag::ValueMask) {
|
|
case Node::Flag::ValuePop:
|
|
return nullptr;
|
|
case Node::Flag::ValueImmediate:
|
|
return &node.value;
|
|
case Node::Flag::ValueLookupDot:
|
|
ptr = convert_dot_to_json_pointer(node.str, ptr_buffer);
|
|
break;
|
|
case Node::Flag::ValueLookupPointer:
|
|
ptr_buffer += '/';
|
|
ptr_buffer += node.str;
|
|
ptr = ptr_buffer;
|
|
break;
|
|
}
|
|
|
|
json::json_pointer json_ptr(ptr.data());
|
|
try {
|
|
// first try to evaluate as a loop variable
|
|
// Using contains() is faster than unsucessful at() and throwing an exception
|
|
if (m_loop_data && m_loop_data->contains(json_ptr)) {
|
|
return &m_loop_data->at(json_ptr);
|
|
}
|
|
return &m_data->at(json_ptr);
|
|
} catch (std::exception &) {
|
|
// try to evaluate as a no-argument callback
|
|
if (auto callback = function_storage.find_callback(node.str, 0)) {
|
|
std::vector<const json *> arguments {};
|
|
m_tmp_val = callback(arguments);
|
|
return &m_tmp_val;
|
|
}
|
|
|
|
throw_renderer_error("variable '" + static_cast<std::string>(node.str) + "' not found", node);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
bool truthy(const json &var) const {
|
|
if (var.empty()) {
|
|
return false;
|
|
} else if (var.is_number()) {
|
|
return (var != 0);
|
|
} else if (var.is_string()) {
|
|
return !var.empty();
|
|
}
|
|
|
|
try {
|
|
return var.get<bool>();
|
|
} catch (json::type_error &e) {
|
|
throw JsonError(e.what());
|
|
}
|
|
}
|
|
|
|
void update_loop_data() {
|
|
LoopLevel &level = m_loop_stack.back();
|
|
|
|
if (level.loop_type == LoopLevel::Type::Array) {
|
|
level.data[static_cast<std::string>(level.value_name)] = level.values.at(level.index); // *level.it;
|
|
} else {
|
|
level.data[static_cast<std::string>(level.key_name)] = level.map_it->first;
|
|
level.data[static_cast<std::string>(level.value_name)] = *level.map_it->second;
|
|
}
|
|
auto &loop_data = level.data["loop"];
|
|
loop_data["index"] = level.index;
|
|
loop_data["index1"] = level.index + 1;
|
|
loop_data["is_first"] = (level.index == 0);
|
|
loop_data["is_last"] = (level.index == level.size - 1);
|
|
}
|
|
|
|
void throw_renderer_error(const std::string &message, const Node& node) {
|
|
size_t pos = node.view.data() - current_template->content.c_str();
|
|
SourceLocation loc = get_source_location(current_template->content, pos);
|
|
throw RenderError(message, loc);
|
|
}
|
|
|
|
struct LoopLevel {
|
|
enum class Type { Map, Array };
|
|
|
|
Type loop_type;
|
|
nonstd::string_view key_name; // variable name for keys
|
|
nonstd::string_view value_name; // variable name for values
|
|
json data; // data with loop info added
|
|
|
|
json values; // values to iterate over
|
|
|
|
// loop over list
|
|
size_t index; // current list index
|
|
size_t size; // length of list
|
|
|
|
// loop over map
|
|
using KeyValue = std::pair<nonstd::string_view, json *>;
|
|
using MapValues = std::vector<KeyValue>;
|
|
MapValues map_values; // values to iterate over
|
|
MapValues::iterator map_it; // iterator over values
|
|
};
|
|
|
|
const TemplateStorage &template_storage;
|
|
const FunctionStorage &function_storage;
|
|
|
|
const Template *current_template;
|
|
std::vector<json> m_stack;
|
|
std::vector<LoopLevel> m_loop_stack;
|
|
json *m_loop_data;
|
|
|
|
const json *m_data;
|
|
std::vector<const json *> m_tmp_args;
|
|
json m_tmp_val;
|
|
|
|
RenderConfig config;
|
|
|
|
public:
|
|
Renderer(const RenderConfig& config, const TemplateStorage &included_templates, const FunctionStorage &callbacks)
|
|
: config(config), template_storage(included_templates), function_storage(callbacks) {
|
|
m_stack.reserve(16);
|
|
m_tmp_args.reserve(4);
|
|
m_loop_stack.reserve(16);
|
|
}
|
|
|
|
void render_to(std::ostream &os, const Template &tmpl, const json &data, json *loop_data = nullptr) {
|
|
current_template = &tmpl;
|
|
m_data = &data;
|
|
m_loop_data = loop_data;
|
|
|
|
for (size_t i = 0; i < tmpl.nodes.size(); ++i) {
|
|
const auto &node = tmpl.nodes[i];
|
|
|
|
switch (node.op) {
|
|
case Node::Op::Nop: {
|
|
break;
|
|
}
|
|
case Node::Op::PrintText: {
|
|
os << node.str;
|
|
break;
|
|
}
|
|
case Node::Op::PrintValue: {
|
|
const json &val = *get_args(node)[0];
|
|
if (val.is_string()) {
|
|
os << val.get_ref<const std::string &>();
|
|
} else {
|
|
os << val.dump();
|
|
}
|
|
pop_args(node);
|
|
break;
|
|
}
|
|
case Node::Op::Push: {
|
|
m_stack.emplace_back(*get_imm(node));
|
|
break;
|
|
}
|
|
case Node::Op::Upper: {
|
|
auto result = get_args(node)[0]->get<std::string>();
|
|
std::transform(result.begin(), result.end(), result.begin(), ::toupper);
|
|
pop_args(node);
|
|
m_stack.emplace_back(std::move(result));
|
|
break;
|
|
}
|
|
case Node::Op::Lower: {
|
|
auto result = get_args(node)[0]->get<std::string>();
|
|
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
|
|
pop_args(node);
|
|
m_stack.emplace_back(std::move(result));
|
|
break;
|
|
}
|
|
case Node::Op::Range: {
|
|
int number = get_args(node)[0]->get<int>();
|
|
std::vector<int> result(number);
|
|
std::iota(std::begin(result), std::end(result), 0);
|
|
pop_args(node);
|
|
m_stack.emplace_back(std::move(result));
|
|
break;
|
|
}
|
|
case Node::Op::Length: {
|
|
const json &val = *get_args(node)[0];
|
|
|
|
size_t result;
|
|
if (val.is_string()) {
|
|
result = val.get_ref<const std::string &>().length();
|
|
} else {
|
|
result = val.size();
|
|
}
|
|
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Sort: {
|
|
auto result = get_args(node)[0]->get<std::vector<json>>();
|
|
std::sort(result.begin(), result.end());
|
|
pop_args(node);
|
|
m_stack.emplace_back(std::move(result));
|
|
break;
|
|
}
|
|
case Node::Op::At: {
|
|
auto args = get_args(node);
|
|
auto result = args[0]->at(args[1]->get<int>());
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::First: {
|
|
auto result = get_args(node)[0]->front();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Last: {
|
|
auto result = get_args(node)[0]->back();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Round: {
|
|
auto args = get_args(node);
|
|
double number = args[0]->get<double>();
|
|
int precision = args[1]->get<int>();
|
|
pop_args(node);
|
|
m_stack.emplace_back(std::round(number * std::pow(10.0, precision)) / std::pow(10.0, precision));
|
|
break;
|
|
}
|
|
case Node::Op::DivisibleBy: {
|
|
auto args = get_args(node);
|
|
int number = args[0]->get<int>();
|
|
int divisor = args[1]->get<int>();
|
|
pop_args(node);
|
|
m_stack.emplace_back((divisor != 0) && (number % divisor == 0));
|
|
break;
|
|
}
|
|
case Node::Op::Odd: {
|
|
int number = get_args(node)[0]->get<int>();
|
|
pop_args(node);
|
|
m_stack.emplace_back(number % 2 != 0);
|
|
break;
|
|
}
|
|
case Node::Op::Even: {
|
|
int number = get_args(node)[0]->get<int>();
|
|
pop_args(node);
|
|
m_stack.emplace_back(number % 2 == 0);
|
|
break;
|
|
}
|
|
case Node::Op::Max: {
|
|
auto args = get_args(node);
|
|
auto result = *std::max_element(args[0]->begin(), args[0]->end());
|
|
pop_args(node);
|
|
m_stack.emplace_back(std::move(result));
|
|
break;
|
|
}
|
|
case Node::Op::Min: {
|
|
auto args = get_args(node);
|
|
auto result = *std::min_element(args[0]->begin(), args[0]->end());
|
|
pop_args(node);
|
|
m_stack.emplace_back(std::move(result));
|
|
break;
|
|
}
|
|
case Node::Op::Not: {
|
|
bool result = !truthy(*get_args(node)[0]);
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::And: {
|
|
auto args = get_args(node);
|
|
bool result = truthy(*args[0]) && truthy(*args[1]);
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Or: {
|
|
auto args = get_args(node);
|
|
bool result = truthy(*args[0]) || truthy(*args[1]);
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::In: {
|
|
auto args = get_args(node);
|
|
bool result = std::find(args[1]->begin(), args[1]->end(), *args[0]) != args[1]->end();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Equal: {
|
|
auto args = get_args(node);
|
|
bool result = (*args[0] == *args[1]);
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Greater: {
|
|
auto args = get_args(node);
|
|
bool result = (*args[0] > *args[1]);
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Less: {
|
|
auto args = get_args(node);
|
|
bool result = (*args[0] < *args[1]);
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::GreaterEqual: {
|
|
auto args = get_args(node);
|
|
bool result = (*args[0] >= *args[1]);
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::LessEqual: {
|
|
auto args = get_args(node);
|
|
bool result = (*args[0] <= *args[1]);
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Different: {
|
|
auto args = get_args(node);
|
|
bool result = (*args[0] != *args[1]);
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Float: {
|
|
double result = std::stod(get_args(node)[0]->get_ref<const std::string &>());
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Int: {
|
|
int result = std::stoi(get_args(node)[0]->get_ref<const std::string &>());
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Exists: {
|
|
auto &&name = get_args(node)[0]->get_ref<const std::string &>();
|
|
bool result = (data.find(name) != data.end());
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::ExistsInObject: {
|
|
auto args = get_args(node);
|
|
auto &&name = args[1]->get_ref<const std::string &>();
|
|
bool result = (args[0]->find(name) != args[0]->end());
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::IsBoolean: {
|
|
bool result = get_args(node)[0]->is_boolean();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::IsNumber: {
|
|
bool result = get_args(node)[0]->is_number();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::IsInteger: {
|
|
bool result = get_args(node)[0]->is_number_integer();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::IsFloat: {
|
|
bool result = get_args(node)[0]->is_number_float();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::IsObject: {
|
|
bool result = get_args(node)[0]->is_object();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::IsArray: {
|
|
bool result = get_args(node)[0]->is_array();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::IsString: {
|
|
bool result = get_args(node)[0]->is_string();
|
|
pop_args(node);
|
|
m_stack.emplace_back(result);
|
|
break;
|
|
}
|
|
case Node::Op::Default: {
|
|
// default needs to be a bit "magic"; we can't evaluate the first
|
|
// argument during the push operation, so we swap the arguments during
|
|
// the parse phase so the second argument is pushed on the stack and
|
|
// the first argument is in the immediate
|
|
try {
|
|
const json *imm = get_imm(node);
|
|
// if no exception was raised, replace the stack value with it
|
|
m_stack.back() = *imm;
|
|
} catch (std::exception &) {
|
|
// couldn't read immediate, just leave the stack as is
|
|
}
|
|
break;
|
|
}
|
|
case Node::Op::Include: {
|
|
auto sub_renderer = Renderer(config, template_storage, function_storage);
|
|
auto include_name = get_imm(node)->get_ref<const std::string &>();
|
|
auto included_template_it = template_storage.find(include_name);
|
|
if (included_template_it != template_storage.end()) {
|
|
sub_renderer.render_to(os, included_template_it->second, *m_data, m_loop_data);
|
|
} else if (config.throw_at_missing_includes) {
|
|
throw_renderer_error("include '" + include_name + "' not found", node);
|
|
}
|
|
break;
|
|
}
|
|
case Node::Op::Callback: {
|
|
auto callback = function_storage.find_callback(node.str, node.args);
|
|
if (!callback) {
|
|
throw_renderer_error("function '" + static_cast<std::string>(node.str) + "' (" +
|
|
std::to_string(static_cast<unsigned int>(node.args)) + ") not found", node);
|
|
}
|
|
json result = callback(get_args(node));
|
|
pop_args(node);
|
|
m_stack.emplace_back(std::move(result));
|
|
break;
|
|
}
|
|
case Node::Op::Jump: {
|
|
i = node.args - 1; // -1 due to ++i in loop
|
|
break;
|
|
}
|
|
case Node::Op::ConditionalJump: {
|
|
if (!truthy(m_stack.back())) {
|
|
i = node.args - 1; // -1 due to ++i in loop
|
|
}
|
|
m_stack.pop_back();
|
|
break;
|
|
}
|
|
case Node::Op::StartLoop: {
|
|
// jump past loop body if empty
|
|
if (m_stack.back().empty()) {
|
|
m_stack.pop_back();
|
|
i = node.args; // ++i in loop will take it past EndLoop
|
|
break;
|
|
}
|
|
|
|
m_loop_stack.emplace_back();
|
|
LoopLevel &level = m_loop_stack.back();
|
|
level.value_name = node.str;
|
|
level.values = std::move(m_stack.back());
|
|
if (m_loop_data) {
|
|
level.data = *m_loop_data;
|
|
}
|
|
level.index = 0;
|
|
m_stack.pop_back();
|
|
|
|
if (node.value.is_string()) {
|
|
// map iterator
|
|
if (!level.values.is_object()) {
|
|
m_loop_stack.pop_back();
|
|
throw_renderer_error("for key, value requires object", node);
|
|
}
|
|
level.loop_type = LoopLevel::Type::Map;
|
|
level.key_name = node.value.get_ref<const std::string &>();
|
|
|
|
// sort by key
|
|
for (auto it = level.values.begin(), end = level.values.end(); it != end; ++it) {
|
|
level.map_values.emplace_back(it.key(), &it.value());
|
|
}
|
|
auto sort_lambda = [](const LoopLevel::KeyValue &a, const LoopLevel::KeyValue &b) {
|
|
return a.first < b.first;
|
|
};
|
|
std::sort(level.map_values.begin(), level.map_values.end(), sort_lambda);
|
|
level.map_it = level.map_values.begin();
|
|
level.size = level.map_values.size();
|
|
} else {
|
|
if (!level.values.is_array()) {
|
|
m_loop_stack.pop_back();
|
|
throw_renderer_error("type must be array", node);
|
|
}
|
|
|
|
// list iterator
|
|
level.loop_type = LoopLevel::Type::Array;
|
|
level.size = level.values.size();
|
|
}
|
|
|
|
// provide parent access in nested loop
|
|
auto parent_loop_it = level.data.find("loop");
|
|
if (parent_loop_it != level.data.end()) {
|
|
json loop_copy = *parent_loop_it;
|
|
(*parent_loop_it)["parent"] = std::move(loop_copy);
|
|
}
|
|
|
|
// set "current" loop data to this level
|
|
m_loop_data = &level.data;
|
|
update_loop_data();
|
|
break;
|
|
}
|
|
case Node::Op::EndLoop: {
|
|
if (m_loop_stack.empty()) {
|
|
throw_renderer_error("unexpected state in renderer", node);
|
|
}
|
|
LoopLevel &level = m_loop_stack.back();
|
|
|
|
bool done;
|
|
level.index += 1;
|
|
if (level.loop_type == LoopLevel::Type::Array) {
|
|
done = (level.index == level.values.size());
|
|
} else {
|
|
level.map_it += 1;
|
|
done = (level.map_it == level.map_values.end());
|
|
}
|
|
|
|
if (done) {
|
|
m_loop_stack.pop_back();
|
|
// set "current" data to outer loop data or main data as appropriate
|
|
if (!m_loop_stack.empty()) {
|
|
m_loop_data = &m_loop_stack.back().data;
|
|
} else {
|
|
m_loop_data = loop_data;
|
|
}
|
|
break;
|
|
}
|
|
|
|
update_loop_data();
|
|
|
|
// jump back to start of loop
|
|
i = node.args - 1; // -1 due to ++i in loop
|
|
break;
|
|
}
|
|
default: {
|
|
throw_renderer_error("unknown operation in renderer: " + std::to_string(static_cast<unsigned int>(node.op)), node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace inja
|
|
|
|
#endif // INCLUDE_INJA_RENDERER_HPP_
|