diff --git a/README.md b/README.md index 8e1daa4..a89bbd2 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ using json = nlohmann::json; ## Tutorial - ### Template Rendering ```c++ json data; @@ -85,12 +84,12 @@ Variables can be rendered within the `{{ ... }}` expressions. ```c++ json data; data["neighbour"] = "Peter"; -data["guests"] = {"Jeff", "Pierre", "Tom"}; +data["guests"] = {"Jeff", "Patrick", "Tom"}; data["time"]["start"] = 16; data["time"]["end"] = 22; // Indexing in array -render("{{ guests/1 }}", data); // "Pierre" +render("{{ guests/1 }}", data); // "Patrick" // Objects render("{{ time/start }} to {{ time/end }}pm"); // "16 to 22pm" @@ -142,6 +141,21 @@ Include other files, relative from the current file location. {% include "footer.html" %} ``` +### Functions + +A few functions are implemented within the inja template syntax. They can be called with +```c++ +// upper() +render("Hello {{ upper(neighbour) }}!", data); // "Hello PETER!" +render("Hello {{ lower(neighbour) }}!", data); // "Hello peter!" + +// Range function, useful for loops +render("{% for i in range(4) %}{{ index1 }}{% endfor %}", data); // "1234" + +// Length function (but please don't combine with range, use list directly...) +render("I count {{ length(guests) }} guests.", data); // "I count 3 guests." +``` + ### Comments Comments can be written with the `{# ... #}` syntax. @@ -149,6 +163,8 @@ Comments can be written with the `{# ... #}` syntax. render("Hello{# Todo #}!", data); // "Hello!" ``` + + ## Supported compilers Currently, the following compilers are tested: @@ -159,6 +175,7 @@ Currently, the following compilers are tested: - Microsoft Visual C++ 2017 / Build Tools 15.1.548.43366 (and possibly later) + ## License The class is licensed under the [MIT License](https://raw.githubusercontent.com/pantor/inja/master/LICENSE). diff --git a/src/inja.hpp b/src/inja.hpp index 1267044..4dc911f 100644 --- a/src/inja.hpp +++ b/src/inja.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -64,13 +65,9 @@ public: class MatchClosed { public: Match open_match, close_match; - std::string inner, outer; MatchClosed() { } - MatchClosed(std::string input, Match open_match, Match close_match): open_match(open_match), close_match(close_match) { - outer = input.substr(position(), length()); - inner = input.substr(open_match.end_position(), close_match.position() - open_match.end_position()); - } + MatchClosed(Match open_match, Match close_match): open_match(open_match), close_match(close_match) { } size_t position() { return open_match.position(); } size_t end_position() { return close_match.end_position(); } @@ -78,6 +75,8 @@ public: bool found() { return open_match.found() and close_match.found(); } std::string prefix() { return open_match.prefix(); } std::string suffix() { return close_match.suffix(); } + std::string outer() { return open_match.str() + static_cast(open_match.suffix()).substr(0, close_match.end_position() - open_match.end_position()); } + std::string inner() { return static_cast(open_match.suffix()).substr(0, close_match.position() - open_match.end_position()); } }; @@ -143,7 +142,7 @@ inline MatchClosed search_closed_match_on_level(const std::string& input, Regex match_delimiter = search(input, regex_statement, current_position); } - return MatchClosed(input, open_match, match_delimiter); + return MatchClosed(open_match, match_delimiter); } inline MatchClosed search_closed_match(std::string input, Regex regex_statement, Regex regex_open, Regex regex_close, Match open_match) { @@ -153,13 +152,6 @@ inline MatchClosed search_closed_match(std::string input, Regex regex_statement, class Parser { public: - enum class Delimiter { - Statement, - LineStatement, - Expression, - Comment - }; - enum class Type { String, Loop, @@ -170,6 +162,13 @@ public: Variable }; + enum class Delimiter { + Statement, + LineStatement, + Expression, + Comment + }; + std::map regex_map_delimiters = { {Delimiter::Statement, Regex{"\\{\\%\\s*(.+?)\\s*\\%\\}"}}, {Delimiter::LineStatement, Regex{"(?:^|\\n)##\\s*(.+)\\s*"}}, @@ -178,6 +177,7 @@ public: }; const Regex regex_loop_open{"for (.*)"}; + const Regex regex_loop_in_list{"for (\\w+) in (.+)"}; const Regex regex_loop_close{"endfor"}; const Regex regex_include{"include \"(.*)\""}; @@ -198,9 +198,10 @@ public: const Regex regex_condition_less_equal{"(.+) <= (.+)"}; const Regex regex_condition_different{"(.+) != (.+)"}; - const Regex regex_function_range{"range\\(\\s*(.*?)\\s*\\)"}; const Regex regex_function_upper{"upper\\(\\s*(.*?)\\s*\\)"}; const Regex regex_function_lower{"lower\\(\\s*(.*?)\\s*\\)"}; + const Regex regex_function_range{"range\\(\\s*(.*?)\\s*\\)"}; + const Regex regex_function_length{"length\\(\\s*(.*?)\\s*\\)"}; Parser() { } @@ -235,7 +236,7 @@ public: current_position = loop_match.end_position(); std::string loop_command = inner_match_delimiter.str(0); - result += element(Type::Loop, {{"command", loop_command}, {"inner", loop_match.inner}}); + result += element(Type::Loop, {{"command", loop_command}, {"inner", loop_match.inner()}}); } // Include else if (std::regex_match(delimiter_inner, inner_match_delimiter, regex_include)) { @@ -252,7 +253,7 @@ public: while (else_if_match.found()) { condition_match = else_if_match.close_match; - condition_result["children"] += element(Type::ConditionBranch, {{"command", else_if_match.open_match.str(1)}, {"inner", else_if_match.inner}}); + condition_result["children"] += element(Type::ConditionBranch, {{"command", else_if_match.open_match.str(1)}, {"inner", else_if_match.inner()}}); else_if_match = search_closed_match_on_level(input, match_delimiter.regex(), regex_condition_open, regex_condition_close, regex_condition_else_if, condition_match); } @@ -261,12 +262,12 @@ public: if (else_match.found()) { condition_match = else_match.close_match; - condition_result["children"] += element(Type::ConditionBranch, {{"command", else_match.open_match.str(1)}, {"inner", else_match.inner}}); + condition_result["children"] += element(Type::ConditionBranch, {{"command", else_match.open_match.str(1)}, {"inner", else_match.inner()}}); } MatchClosed last_if_match = search_closed_match(input, match_delimiter.regex(), regex_condition_open, regex_condition_close, condition_match); - condition_result["children"] += element(Type::ConditionBranch, {{"command", last_if_match.open_match.str(1)}, {"inner", last_if_match.inner}}); + condition_result["children"] += element(Type::ConditionBranch, {{"command", last_if_match.open_match.str(1)}, {"inner", last_if_match.inner()}}); current_position = last_if_match.end_position(); result += condition_result; @@ -282,9 +283,7 @@ public: result += element(Type::Comment, {{"text", delimiter_inner}}); break; } - default: { - throw std::runtime_error("Parser error: Unknown delimiter."); - } + default: { throw std::runtime_error("Parser error: Unknown delimiter."); } } match_delimiter = search(input, regex_delimiters, current_position); @@ -315,13 +314,11 @@ public: }; - class Environment { std::string global_path; Parser parser; - public: Environment(): Environment("./") { } Environment(std::string global_path): global_path(global_path), parser() { } @@ -330,16 +327,41 @@ public: parser.regex_map_delimiters[Parser::Delimiter::Expression] = Regex{open + close}; } - json parse_variable(std::string input, json data) { - return parse_variable(input, data, true); + json eval_variable(std::string input, json data) { + return eval_variable(input, data, true); } - json parse_variable(std::string input, json data, bool throw_error) { + json eval_variable(std::string input, json data, bool throw_error) { // Json Raw Data if ( json::accept(input) ) { return json::parse(input); } - // TODO Implement functions - + Match match_function; + if (std::regex_match(input, match_function, parser.regex_function_upper)) { + json str = eval_variable(match_function.str(1), data); + if (not str.is_string()) { throw std::runtime_error("Argument in upper function is not a string."); } + std::string data = str.get(); + std::transform(data.begin(), data.end(), data.begin(), toupper); + return data; + } + else if (std::regex_match(input, match_function, parser.regex_function_lower)) { + json str = eval_variable(match_function.str(1), data); + if (not str.is_string()) { throw std::runtime_error("Argument in lower function is not a string."); } + std::string data = str.get(); + std::transform(data.begin(), data.end(), data.begin(), tolower); + return data; + } + else if (std::regex_match(input, match_function, parser.regex_function_range)) { + json number = eval_variable(match_function.str(1), data); + if (not number.is_number()) { throw std::runtime_error("Argument in range function is not a number."); } + std::vector result(number.get()); + std::iota(std::begin(result), std::end(result), 0); + return result; + } + else if (std::regex_match(input, match_function, parser.regex_function_length)) { + json list = eval_variable(match_function.str(1), data); + if (not list.is_array()) { throw std::runtime_error("Argument in length function is not a list."); } + return list.size(); + } if (input[0] != '/') { input.insert(0, "/"); } json::json_pointer ptr(input); @@ -349,59 +371,59 @@ public: return result; } - bool parse_condition(std::string condition, json data) { + bool eval_condition(std::string condition, json data) { Match match_condition; if (std::regex_match(condition, match_condition, parser.regex_condition_not)) { - return not parse_condition(match_condition.str(1), data); + return not eval_condition(match_condition.str(1), data); } else if (std::regex_match(condition, match_condition, parser.regex_condition_and)) { - return (parse_condition(match_condition.str(1), data) and parse_condition(match_condition.str(2), data)); + return (eval_condition(match_condition.str(1), data) and eval_condition(match_condition.str(2), data)); } else if (std::regex_match(condition, match_condition, parser.regex_condition_or)) { - return (parse_condition(match_condition.str(1), data) or parse_condition(match_condition.str(2), data)); + return (eval_condition(match_condition.str(1), data) or eval_condition(match_condition.str(2), data)); } else if (std::regex_match(condition, match_condition, parser.regex_condition_in)) { - json item = parse_variable(match_condition.str(1), data); - json list = parse_variable(match_condition.str(2), data); + json item = eval_variable(match_condition.str(1), data); + json list = eval_variable(match_condition.str(2), data); return (std::find(list.begin(), list.end(), item) != list.end()); } else if (std::regex_match(condition, match_condition, parser.regex_condition_equal)) { - json comp1 = parse_variable(match_condition.str(1), data); - json comp2 = parse_variable(match_condition.str(2), data); + json comp1 = eval_variable(match_condition.str(1), data); + json comp2 = eval_variable(match_condition.str(2), data); return comp1 == comp2; } else if (std::regex_match(condition, match_condition, parser.regex_condition_greater)) { - json comp1 = parse_variable(match_condition.str(1), data); - json comp2 = parse_variable(match_condition.str(2), data); + json comp1 = eval_variable(match_condition.str(1), data); + json comp2 = eval_variable(match_condition.str(2), data); return comp1 > comp2; } else if (std::regex_match(condition, match_condition, parser.regex_condition_less)) { - json comp1 = parse_variable(match_condition.str(1), data); - json comp2 = parse_variable(match_condition.str(2), data); + json comp1 = eval_variable(match_condition.str(1), data); + json comp2 = eval_variable(match_condition.str(2), data); return comp1 < comp2; } else if (std::regex_match(condition, match_condition, parser.regex_condition_greater_equal)) { - json comp1 = parse_variable(match_condition.str(1), data); - json comp2 = parse_variable(match_condition.str(2), data); + json comp1 = eval_variable(match_condition.str(1), data); + json comp2 = eval_variable(match_condition.str(2), data); return comp1 >= comp2; } else if (std::regex_match(condition, match_condition, parser.regex_condition_less_equal)) { - json comp1 = parse_variable(match_condition.str(1), data); - json comp2 = parse_variable(match_condition.str(2), data); + json comp1 = eval_variable(match_condition.str(1), data); + json comp2 = eval_variable(match_condition.str(2), data); return comp1 <= comp2; } else if (std::regex_match(condition, match_condition, parser.regex_condition_different)) { - json comp1 = parse_variable(match_condition.str(1), data); - json comp2 = parse_variable(match_condition.str(2), data); + json comp1 = eval_variable(match_condition.str(1), data); + json comp2 = eval_variable(match_condition.str(2), data); return comp1 != comp2; } - json var = parse_variable(condition, data, false); + json var = eval_variable(condition, data, false); if (var.empty()) { return false; } else if (var.is_boolean()) { return var; } else if (var.is_number()) { return (var != 0); } else if (var.is_string()) { return not var.empty(); } - return false; + return true; } std::string render_json(json data) { @@ -421,7 +443,7 @@ public: break; } case Parser::Type::Variable: { - json variable = parse_variable(element["command"], data); + json variable = eval_variable(element["command"], data); result += render_json(variable); break; } @@ -430,15 +452,13 @@ public: break; } case Parser::Type::Loop: { - const Regex regex_loop_list{"for (\\w+) in (.+)"}; - std::string command = element["command"].get(); Match match_command; - if (std::regex_match(command, match_command, regex_loop_list)) { + if (std::regex_match(command, match_command, parser.regex_loop_in_list)) { std::string item_name = match_command.str(1); std::string list_name = match_command.str(2); - json list = parse_variable(list_name, data); + json list = eval_variable(list_name, data); for (int i = 0; i < list.size(); i++) { json data_loop = data; data_loop[item_name] = list[i]; @@ -449,9 +469,7 @@ public: result += render_tree(element["children"], data_loop, path); } } - else { - throw std::runtime_error("Unknown loop in renderer."); - } + else { throw std::runtime_error("Unknown loop in renderer."); } break; } case Parser::Type::Condition: { @@ -464,23 +482,17 @@ public: std::string condition_type = match_command.str(1); std::string condition = match_command.str(2); - if (parse_condition(condition, data) || condition_type == "else") { + if (eval_condition(condition, data) || condition_type == "else") { result += render_tree(branch["children"], data, path); break; } } - else { - throw std::runtime_error("Unknown condition in renderer."); - } + else { throw std::runtime_error("Unknown condition in renderer."); } } break; } - case Parser::Type::Comment: { - break; - } - default: { - throw std::runtime_error("Unknown type in renderer."); - } + case Parser::Type::Comment: { break; } + default: { throw std::runtime_error("Unknown type in renderer."); } } } return result; diff --git a/test/src/unit-parser.cpp b/test/src/unit-parser.cpp index 8e37f84..87911d6 100644 --- a/test/src/unit-parser.cpp +++ b/test/src/unit-parser.cpp @@ -129,7 +129,7 @@ lorem ipsum } } -TEST_CASE("Parse json") { +TEST_CASE("Parse variables") { inja::Environment env = inja::Environment(); json data; @@ -142,18 +142,23 @@ TEST_CASE("Parse json") { data["brother"]["daughter0"] = { { "name", "Maria" } }; SECTION("Variables from values") { - CHECK( env.parse_variable("42", data) == 42 ); - CHECK( env.parse_variable("3.1415", data) == 3.1415 ); - CHECK( env.parse_variable("\"hello\"", data) == "hello" ); + CHECK( env.eval_variable("42", data) == 42 ); + CHECK( env.eval_variable("3.1415", data) == 3.1415 ); + CHECK( env.eval_variable("\"hello\"", data) == "hello" ); + CHECK( env.eval_variable("true", data) == true ); + CHECK( env.eval_variable("[5, 6, 8]", data) == std::vector({5, 6, 8}) ); } SECTION("Variables from JSON data") { - CHECK( env.parse_variable("name", data) == "Peter" ); - CHECK( env.parse_variable("age", data) == 29 ); - CHECK( env.parse_variable("names/1", data) == "Seb" ); - CHECK( env.parse_variable("brother/name", data) == "Chris" ); - CHECK( env.parse_variable("brother/daughters/0", data) == "Maria" ); - CHECK_THROWS_WITH( env.parse_variable("noelement", data), "JSON pointer found no element." ); + CHECK( env.eval_variable("name", data) == "Peter" ); + CHECK( env.eval_variable("age", data) == 29 ); + CHECK( env.eval_variable("names/1", data) == "Seb" ); + CHECK( env.eval_variable("brother/name", data) == "Chris" ); + CHECK( env.eval_variable("brother/daughters/0", data) == "Maria" ); + CHECK( env.eval_variable("/age", data) == 29 ); + + CHECK_THROWS_WITH( env.eval_variable("noelement", data), "JSON pointer found no element." ); + CHECK_THROWS_WITH( env.eval_variable("&4s-", data), "JSON pointer found no element." ); } } @@ -167,37 +172,70 @@ TEST_CASE("Parse conditions") { data["guests"] = {"Jeff", "Seb"}; SECTION("Elements") { - CHECK( env.parse_condition("age", data) ); - CHECK_FALSE( env.parse_condition("size", data) ); + CHECK( env.eval_condition("age", data) ); + CHECK( env.eval_condition("guests", data) ); + CHECK_FALSE( env.eval_condition("size", data) ); + CHECK_FALSE( env.eval_condition("false", data) ); } SECTION("Operators") { - CHECK( env.parse_condition("not size", data) ); - CHECK_FALSE( env.parse_condition("not true", data) ); - CHECK( env.parse_condition("true and true", data) ); - CHECK( env.parse_condition("true or false", data) ); - CHECK_FALSE( env.parse_condition("true and not true", data) ); + CHECK( env.eval_condition("not size", data) ); + CHECK_FALSE( env.eval_condition("not true", data) ); + CHECK( env.eval_condition("true and true", data) ); + CHECK( env.eval_condition("true or false", data) ); + CHECK_FALSE( env.eval_condition("true and not true", data) ); } SECTION("Numbers") { - CHECK( env.parse_condition("age == 29", data) ); - CHECK( env.parse_condition("age >= 29", data) ); - CHECK( env.parse_condition("age <= 29", data) ); - CHECK( env.parse_condition("age < 100", data) ); - CHECK_FALSE( env.parse_condition("age > 29", data) ); - CHECK_FALSE( env.parse_condition("age != 29", data) ); - CHECK_FALSE( env.parse_condition("age < 28", data) ); - CHECK_FALSE( env.parse_condition("age < -100.0", data) ); + CHECK( env.eval_condition("age == 29", data) ); + CHECK( env.eval_condition("age >= 29", data) ); + CHECK( env.eval_condition("age <= 29", data) ); + CHECK( env.eval_condition("age < 100", data) ); + CHECK_FALSE( env.eval_condition("age > 29", data) ); + CHECK_FALSE( env.eval_condition("age != 29", data) ); + CHECK_FALSE( env.eval_condition("age < 28", data) ); + CHECK_FALSE( env.eval_condition("age < -100.0", data) ); } SECTION("Strings") { - CHECK( env.parse_condition("brother == father", data) ); - CHECK( env.parse_condition("brother == \"Peter\"", data) ); - CHECK_FALSE( env.parse_condition("not brother == father", data) ); + CHECK( env.eval_condition("brother == father", data) ); + CHECK( env.eval_condition("brother == \"Peter\"", data) ); + CHECK_FALSE( env.eval_condition("not brother == father", data) ); } SECTION("Lists") { - // CHECK( env.parse_condition("\"Jeff\" in guests", data) ); - CHECK_FALSE( env.parse_condition("brother in guests", data) ); + CHECK( env.eval_condition("\"Jeff\" in guests", data) ); + CHECK_FALSE( env.eval_condition("brother in guests", data) ); + } +} + +TEST_CASE("Parse functions") { + inja::Environment env = inja::Environment(); + + json data; + data["name"] = "Peter"; + data["city"] = "New York"; + data["names"] = {"Jeff", "Seb", "Peter", "Tom"}; + + SECTION("Upper") { + CHECK( env.eval_variable("upper(name)", data) == "PETER" ); + CHECK( env.eval_variable("upper(city)", data) == "NEW YORK" ); + CHECK_THROWS_WITH( env.eval_variable("upper(5)", data), "Argument in upper function is not a string." ); + } + + SECTION("Lower") { + CHECK( env.eval_variable("lower(name)", data) == "peter" ); + CHECK( env.eval_variable("lower(city)", data) == "new york" ); + CHECK_THROWS_WITH( env.eval_variable("lower(5)", data), "Argument in lower function is not a string." ); + } + + SECTION("Range") { + CHECK( env.eval_variable("range(4)", data) == std::vector({0, 1, 2, 3}) ); + CHECK_THROWS_WITH( env.eval_variable("range(true)", data), "Argument in range function is not a number." ); + } + + SECTION("Length") { + CHECK( env.eval_variable("length(names)", data) == 4 ); + CHECK_THROWS_WITH( env.eval_variable("length(5)", data), "Argument in length function is not a list." ); } } diff --git a/test/src/unit-string-helper.cpp b/test/src/unit-string-helper.cpp index 9ab6b01..1e1c8f2 100644 --- a/test/src/unit-string-helper.cpp +++ b/test/src/unit-string-helper.cpp @@ -79,7 +79,7 @@ TEST_CASE("Search on level") { CHECK( match.close_match.end_position() == 28 ); CHECK( match.position() == 8 ); CHECK( match.end_position() == 28 ); - CHECK( match.outer == "(% up %)Test(% N1 %)" ); - CHECK( match.inner == "Test" ); + CHECK( match.outer() == "(% up %)Test(% N1 %)" ); + CHECK( match.inner() == "Test" ); } }