From ee2451edae76d74461e03d73ec26cb24ae475302 Mon Sep 17 00:00:00 2001 From: pantor Date: Sat, 17 Feb 2018 14:06:38 +0100 Subject: [PATCH] add callbacks --- README.md | 28 ++++++++++++++++++ src/inja.hpp | 60 +++++++++++++++++++++++++++++--------- test/src/unit-renderer.cpp | 36 +++++++++++++++++++++-- 3 files changed, 109 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d73a679..98ab1f7 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,34 @@ render({{ even(42) }}, data); // true render({{ divisibleBy(42, 7) }}, data); // true ``` +### Callbacks + +You can create your own functions with callbacks. They can be added to the environment like +```c++ +Environment env = Environment(); + +/* + * Callbacks are defined by its: + * - name, which is equal to its function name + * - number of arguments + * - callback function. Using std::function, you can e.g. also use lambdas. + */ +env.add_callback("double", 1, [&env](Parsed::Arguments args, json data) { + const int number = env.renderer.eval_expression(args[0], data); // Adapt the type and index of the argument + return 2 * number; +}); + +// You can then use a callback like a regular function +env.render("{{ double(16) }}", data) // "32" + +// A callback without argument can be used like a variable: +std::string greet = "Hello"; +env.add_callback("double-greetings", 0, [&env, greet](Parsed::Arguments args, json data) { + return greet + " " + greet + "!"; +}); +env.render("{{ double-greetings }}", data) // "Hello Hello!" +``` + ### Comments Comments can be written with the `{# ... #}` syntax. diff --git a/src/inja.hpp b/src/inja.hpp index 0ed29d0..4158e25 100644 --- a/src/inja.hpp +++ b/src/inja.hpp @@ -193,7 +193,7 @@ inline MatchType match(const std::string& input, std::map regexes) return match; } } - throw std::runtime_error("Could not match input: " + input); + // throw std::runtime_error("Could not match input: " + input); return match; } @@ -249,7 +249,8 @@ struct Parsed { DivisibleBy, Odd, Even, - ReadJson + ReadJson, + Callback }; enum class Condition { @@ -291,6 +292,7 @@ struct Parsed { explicit ElementExpression(): ElementExpression(Function::ReadJson) { } explicit ElementExpression(const Function function_): Element(Type::Expression), function(function_), args({}), command("") { } }; + typedef std::vector Arguments; struct ElementLoop: public Element { const std::string item; @@ -317,10 +319,16 @@ struct Parsed { class Template { public: const Parsed::Element parsed_template; + + explicit Template(const Parsed::Element& parsed_template): parsed_template(parsed_template) { } +}; + + +class Renderer { +public: ElementNotation elementNotation; - explicit Template(const Parsed::Element& parsed_template): Template(parsed_template, ElementNotation::Pointer) { } - explicit Template(const Parsed::Element& parsed_template, ElementNotation elementNotation): parsed_template(parsed_template), elementNotation(elementNotation) { } + std::map> map_callbacks; template bool eval_expression(const Parsed::ElementExpression& element, json data) { @@ -332,7 +340,7 @@ public: } template - T eval_expression(const Parsed::ElementExpression& element, json data) { + T eval_expression(const Parsed::ElementExpression& element, json data) { return eval_function(element, data).get(); } @@ -435,15 +443,18 @@ public: if (result.is_null()) { throw std::runtime_error("Did not found json element: " + element.command); } return result; } + case Parsed::Function::Callback: { + return map_callbacks[element.command](element.args, data); + } } throw std::runtime_error("Unknown function in renderer."); return json(); } - std::string render(json data) { + std::string render(Template temp, json data) { std::string result = ""; - for (auto element: parsed_template.children) { + for (auto element: temp.parsed_template.children) { switch (element->type) { case Parsed::Type::Main: { throw std::runtime_error("Main type in renderer."); } case Parsed::Type::String: { @@ -476,7 +487,7 @@ public: data_loop["index1"] = i + 1; data_loop["is_first"] = (i == 0); data_loop["is_last"] = (i == list.size() - 1); - result += Template(*elementLoop, elementNotation).render(data_loop); + result += render(Template(*elementLoop), data_loop); } break; } @@ -485,7 +496,7 @@ public: for (auto branch: elementCondition->children) { auto elementBranch = std::static_pointer_cast(branch); if (elementBranch->condition_type == Parsed::Condition::Else || eval_expression(elementBranch->condition, data)) { - result += Template(*elementBranch, elementNotation).render(data); + result += render(Template(*elementBranch), data); break; } } @@ -567,9 +578,24 @@ public: {Parsed::Function::ReadJson, Regex{"\\s*([^\\(\\)]*\\S)\\s*"}} }; + std::map regex_map_callbacks; + Parser() { } Parsed::ElementExpression parse_expression(const std::string& input) { + MatchType match_callback = match(input, regex_map_callbacks); + if (!match_callback.type().empty()) { + std::vector args = {}; + for (unsigned int i = 1; i < match_callback.size(); i++) { // str(0) is whole group + args.push_back( parse_expression(match_callback.str(i)) ); + } + + Parsed::ElementExpression result = Parsed::ElementExpression(Parsed::Function::Callback); + result.args = args; + result.command = match_callback.type(); + return result; + } + MatchType match_function = match(input, regex_map_functions); switch ( match_function.type() ) { case Parsed::Function::ReadJson: { @@ -738,6 +764,9 @@ class Environment { Parser parser = Parser(); public: + Renderer renderer = Renderer(); + + Environment(): Environment("./") { } explicit Environment(const std::string& global_path): input_path(global_path), output_path(global_path), parser() { } explicit Environment(const std::string& input_path, const std::string& output_path): input_path(input_path), output_path(output_path), parser() { } @@ -765,23 +794,23 @@ public: Template parse(const std::string& input) { Template parsed = parser.parse(input); - parsed.elementNotation = elementNotation; return parsed; } Template parse_template(const std::string& filename) { Template parsed = parser.parse_template(input_path + filename); - parsed.elementNotation = elementNotation; return parsed; } std::string render(const std::string& input, json data) { const std::string text = input; - return parse(text).render(data); + renderer.elementNotation = elementNotation; + return renderer.render(parse(text), data); } std::string render_template(const std::string& filename, json data) { - return parse_template(filename).render(data); + renderer.elementNotation = elementNotation; + return renderer.render(parse_template(filename), data); } std::string render_template_with_json_file(const std::string& filename, const std::string& filename_data) { @@ -804,6 +833,11 @@ public: return parser.load_file(input_path + filename); } + void add_callback(std::string name, int number_arguments, std::function callback) { + parser.regex_map_callbacks[name] = Parser::function_regex(name, number_arguments); + renderer.map_callbacks[name] = callback; + } + json load_json(const std::string& filename) { std::ifstream file(input_path + filename); json j; diff --git a/test/src/unit-renderer.cpp b/test/src/unit-renderer.cpp index 623ec8e..7e41a53 100644 --- a/test/src/unit-renderer.cpp +++ b/test/src/unit-renderer.cpp @@ -135,6 +135,38 @@ TEST_CASE("functions") { } } +TEST_CASE("callbacks") { + inja::Environment env = inja::Environment(); + json data; + data["age"] = 28; + + env.add_callback("double", 1, [&env](inja::Parsed::Arguments args, json data) { + const int number = env.renderer.eval_expression(args[0], data); + return 2 * number; + }); + + env.add_callback("half", 1, [&env](inja::Parsed::Arguments args, json data) { + const int number = env.renderer.eval_expression(args[0], data); + return number / 2; + }); + + std::string greet = "Hello"; + env.add_callback("double-greetings", 0, [&env, greet](inja::Parsed::Arguments args, json data) { + return greet + " " + greet + "!"; + }); + + env.add_callback("multiply", 2, [&env](inja::Parsed::Arguments args, json data) { + const double number1 = env.renderer.eval_expression(args[0], data); + const double number2 = env.renderer.eval_expression(args[1], data); + return number1 * number2; + }); + + CHECK( env.render("{{ double(age) }}", data) == "56" ); + CHECK( env.render("{{ half(age) }}", data) == "14" ); + CHECK( env.render("{{ double-greetings }}", data) == "Hello Hello!" ); + CHECK( env.render("{{ multiply(4, 5) }}", data) == "20.0" ); +} + TEST_CASE("combinations") { inja::Environment env = inja::Environment(); json data; @@ -161,11 +193,11 @@ TEST_CASE("templates") { data["city"] = "Brunswick"; data["is_happy"] = true; - CHECK( temp.render(data) == "Peter" ); + CHECK( env.renderer.render(temp, data) == "Peter" ); data["is_happy"] = false; - CHECK( temp.render(data) == "Brunswick" ); + CHECK( env.renderer.render(temp, data) == "Brunswick" ); } TEST_CASE("other-syntax") {