diff --git a/README.md b/README.md index a3f3031..7ac3a85 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # Inja -A Template Engine for Modern C++ \ No newline at end of file +A Template Engine for Modern C++ + +[![Github Issues](https://img.shields.io/github/issues/pantor/inja.svg)](http://github.com/pantor/inja/issues) +[![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/pantor/inja/master/LICENSE.MIT) \ No newline at end of file diff --git a/src/inja.hpp b/src/inja.hpp index bbc4421..31802a6 100644 --- a/src/inja.hpp +++ b/src/inja.hpp @@ -12,151 +12,402 @@ using json = nlohmann::json; using string = std::string; - -template -std::basic_string regex_replace(BidirIt first, BidirIt last, - const std::basic_regex& re, UnaryFunction f) -{ - std::basic_string s; - - typename std::match_results::difference_type positionOfLastMatch = 0; - auto endOfLastMatch = first; - - auto callback = [&](const std::match_results& match) +string join_strings(std::vector vector, string delimiter) { + std::stringstream ss; + for (size_t i = 0; i < vector.size(); ++i) { - auto positionOfThisMatch = match.position(0); - auto diff = positionOfThisMatch - positionOfLastMatch; - - auto startOfThisMatch = endOfLastMatch; - std::advance(startOfThisMatch, diff); - - s.append(endOfLastMatch, startOfThisMatch); - s.append(f(match)); - - auto lengthOfMatch = match.length(0); - - positionOfLastMatch = positionOfThisMatch + lengthOfMatch; - - endOfLastMatch = startOfThisMatch; - std::advance(endOfLastMatch, lengthOfMatch); - }; - - std::sregex_iterator begin(first, last, re), end; - std::for_each(begin, end, callback); - - s.append(endOfLastMatch, last); - - return s; + if (i != 0) ss << delimiter; + ss << vector[i]; + } + return ss.str(); } -template -std::string regex_replace(const std::string& s, - const std::basic_regex& re, UnaryFunction f) -{ - return regex_replace(s.cbegin(), s.cend(), re, f); + +struct SearchMatch { + SearchMatch() { } + + SearchMatch(std::smatch match, size_t offset): SearchMatch(match, offset, 1, -1) { } + + SearchMatch(std::smatch match, size_t offset, int inner_group, int regex_number): inner_group(inner_group), regex_number(regex_number) { + position = offset + match.position(); + length = match.length(); + end_position = position + length; + found = !match.empty(); + outer = match[0].str(); + inner = match[inner_group].str(); + prefix = match.prefix(); + suffix = match.suffix(); + } + + string outer, inner, prefix, suffix; + size_t position, end_position; + int length; + bool found = false; + int regex_number = -1, inner_group = 1; +}; + +struct SearchClosedMatch { + SearchClosedMatch() { } + + SearchClosedMatch(string input, SearchMatch open_match, SearchMatch close_match): open_match(open_match), close_match(close_match) { + position = open_match.position; + length = close_match.end_position - open_match.position; + end_position = close_match.end_position; + found = open_match.found && close_match.found; + outer = input.substr(position, length); + inner = input.substr(open_match.end_position, close_match.position - open_match.end_position); + prefix = open_match.prefix; + suffix = close_match.suffix; + } + + string outer, inner, prefix, suffix; + size_t position, end_position; + int length; + bool found = false; + + SearchMatch open_match, close_match; +}; + +SearchMatch search(string input, std::regex regex, size_t position) { + auto first = input.cbegin(); + auto last = input.cend(); + + if (position >= input.length()) { + return SearchMatch(); + } + + std::smatch match; + std::regex_search(first + position, last, match, regex); + return SearchMatch(match, position); } +SearchMatch search(string input, std::vector regex_patterns, size_t position) { + auto first = input.cbegin(); + auto last = input.cend(); + + string regex_pattern = "(" + join_strings(regex_patterns, ")|(") + ")"; + std::regex regex(regex_pattern); + + if (position >= input.length()) { + return SearchMatch(); + } + + // Vector of id vs groups + std::vector regex_mark_counts; + for (int i = 0; i < regex_patterns.size(); i++) { + for (int j = 0; j < std::regex(regex_patterns[i]).mark_count() + 1; j++) { + regex_mark_counts.push_back(i); + } + } + + std::smatch match; + std::regex_search(first + position, last, match, regex); + + int number_regex = -1; + int number_inner = 1; + for (int i = 1; i < match.size(); i++) { + if (match.length(i) > 0) { + number_inner = i + 1; + number_regex = regex_mark_counts[i]; + break; + } + } + + return SearchMatch(match, position, number_inner, number_regex); +} + +SearchClosedMatch search_on_level(string input, std::regex regex_statement, std::regex regex_level_up, std::regex regex_level_down, std::regex regex_search, SearchMatch open_match) { + + int level = 0; + size_t current_position = open_match.end_position; + SearchMatch statement_match = search(input, regex_statement, current_position); + while (statement_match.found) { + current_position = statement_match.end_position; + + if (level == 0 && std::regex_match(statement_match.inner, regex_search)) break; + if (std::regex_match(statement_match.inner, regex_level_up)) level += 1; + else if (std::regex_match(statement_match.inner, regex_level_down)) level -= 1; + + statement_match = search(input, regex_statement, current_position); + } + + return SearchClosedMatch(input, open_match, statement_match); +} + +SearchClosedMatch search_close(string input, std::regex regex_statement, std::regex regex_open, std::regex regex_close, SearchMatch open_match) { + return search_on_level(input, regex_statement, regex_open, regex_close, regex_close, open_match); +} + + + class Environment { - - + std::regex regex_statement; + std::regex regex_line_statement; + std::regex regex_expression; + std::regex regex_comment; + std::vector regex_pattern_delimiters; public: Environment() { - + const string regex_pattern_statement = "\\(\\%\\s*(.+?)\\s*\\%\\)"; + // const string regex_pattern_line_statement = "^## (.*)$"; + const string regex_pattern_expression = "\\{\\{\\s*(.+?)\\s*\\}\\}"; + const string regex_pattern_comment = "\\{#\\s*(.*?)\\s*#\\}"; + + regex_statement = std::regex(regex_pattern_statement); + // regex_line_statement = std::regex(regex_pattern_line_statement); + regex_expression = std::regex(regex_pattern_expression); + regex_comment = std::regex(regex_pattern_comment); + regex_pattern_delimiters = { regex_pattern_statement, regex_pattern_expression, regex_pattern_comment }; } - json get_variable_data(string variable_name, json data) { - // Json Raw Data - if ( json::accept(variable_name) ) { - return json::parse(variable_name); - } - - // Implement range function - std::regex range_regex("^range\\((\\d+)\\)$"); - std::smatch range_match; - if (std::regex_match(variable_name, range_match, range_regex)) { - int counter = std::stoi(range_match[1].str()); - std::vector range(counter); - std::iota(range.begin(), range.end(), 0); - return range; - } - if (variable_name[0] != '/') { - variable_name = "/" + variable_name; - } + json parse_level(string input) { + json result; - json::json_pointer ptr(variable_name); - json result = data[ptr]; - if (result.is_null()) { - throw std::runtime_error("JSON pointer found no element."); - } - return result; - } - - - string render(string template_input, json data) { - return render(template_input, data, "./"); - } - - string render(string template_input, json data, string template_path) { - string result = template_input; - - const std::regex include_regex("\\(\\% include \"(.*)\" \\%\\)"); - result = inja::regex_replace(result, include_regex, - [this, template_path](const std::smatch& match) { - string filename = template_path + match[1].str(); - return load_template(filename); + size_t current_position = 0; + SearchMatch statement_match = search(input, regex_pattern_delimiters, current_position); + while (statement_match.found) { + current_position = statement_match.end_position; + if (!statement_match.prefix.empty()) { + result.push_back({{"type", "string"}, {"text", statement_match.prefix}}); } - ); - - const std::regex loop_regex("\\(\\% for (\\w+) in (.+) \\%\\)(.*)\\(\\% endfor \\%\\)"); - result = inja::regex_replace(result, loop_regex, - [this, data](const std::smatch& match) { - string result = ""; - string entry_name = match[1].str(); - string list_name = match[2].str(); - string inner_string = match[3].str(); - json list = get_variable_data(list_name, data); - if (!list.is_array()) throw std::runtime_error("JSON variable is not a list."); + + // Regex matched a statement "(% ... %)" + if (statement_match.regex_number == 0) { + const std::regex regex_loop_open("for (.*)"); + const std::regex regex_loop_close("endfor"); - for (int i = 0; i < list.size(); i++) { - const std::regex entry_regex("\\{\\{.*" + entry_name + ".*\\}\\}"); - result += inja::regex_replace(inner_string, entry_regex, - [list_name, i](const std::smatch& match) { - return "{{ " + list_name + "/" + std::to_string(i) + " }}"; - } - ); + const std::regex regex_include("include \"(.*)\""); + + const std::regex regex_condition_open("if (.*)"); + const std::regex regex_condition_else_if("else if (.*)"); + const std::regex regex_condition_else("else"); + const std::regex regex_condition_close("endif"); + + std::smatch inner_statement_match; + if (std::regex_match(statement_match.inner, inner_statement_match, regex_loop_open)) { + SearchClosedMatch loop_match = search_close(input, regex_statement, regex_loop_open, regex_loop_close, statement_match); + + current_position = loop_match.end_position; + string loop_command = inner_statement_match[0].str(); + result.push_back({{"type", "loop"}, {"command", loop_command}, {"inner", loop_match.inner}}); } - return result; - } - ); - - const std::regex condition_regex("\\(\\% if (\\w+) \\%\\)(.*)\\(\\% endif \\%\\)"); - result = inja::regex_replace(result, condition_regex, - [this, data](const std::smatch& match) { - string condition_variable_name = match[1].str(); - string inner_string = match[2].str(); - if (get_variable_data(condition_variable_name, data)) { - return inner_string; + else if (std::regex_match(statement_match.inner, inner_statement_match, regex_include)) { + string include_command = inner_statement_match[0].str(); + string filename = inner_statement_match[1].str(); + result.push_back({{"type", "include"}, {"filename", filename}}); + } + else if (std::regex_match(statement_match.inner, inner_statement_match, regex_condition_open)) { + string if_command = inner_statement_match[0].str(); + json condition_result = {{"type", "condition"}, {"children", json::array()}}; + + + SearchMatch condition_match = statement_match; + + SearchClosedMatch else_if_match = search_on_level(input, regex_statement, regex_condition_open, regex_condition_close, regex_condition_else_if, condition_match); + while (else_if_match.found) { + condition_match = else_if_match.close_match; + + condition_result["children"].push_back({{"type", "condition_branch"}, {"command", else_if_match.open_match.inner}, {"inner", else_if_match.inner}}); + + else_if_match = search_on_level(input, regex_statement, regex_condition_open, regex_condition_close, regex_condition_else_if, condition_match); + } + + SearchClosedMatch else_match = search_on_level(input, regex_statement, regex_condition_open, regex_condition_close, regex_condition_else, condition_match); + if (else_match.found) { + condition_match = else_match.close_match; + + condition_result["children"].push_back({{"type", "condition_branch"}, {"command", else_match.open_match.inner}, {"inner", else_match.inner}}); + } + + SearchClosedMatch last_if_match = search_close(input, regex_statement, regex_condition_open, regex_condition_close, condition_match); + + condition_result["children"].push_back({{"type", "condition_branch"}, {"command", last_if_match.open_match.inner}, {"inner", last_if_match.inner}}); + + current_position = last_if_match.end_position; + result.push_back(condition_result); } - return string(""); } - ); - - const std::regex variable_regex("\\{\\{\\s*([^\\}]*[^\\s])\\s*\\}\\}"); - result = inja::regex_replace(result, variable_regex, - [this, data](const std::smatch& match) { - string variable_name = match[1].str(); - return get_variable_data(variable_name, data); + // Regex matched an expression "{{ ... }}" + else if (statement_match.regex_number == 1) { + result.push_back({{"type", "variable"}, {"command", statement_match.inner}}); } - ); + // Regex matched an comment "{# ... #}" + else if (statement_match.regex_number == 2) { + result.push_back({{"type", "comment"}, {"text", statement_match.inner}}); + } + + statement_match = search(input, regex_pattern_delimiters, current_position); + } + if (current_position < input.length()) { + result.push_back({{"type", "string"}, {"text", input.substr(current_position)}}); + } return result; } + json parse_tree(json current_element) { + if (current_element.find("inner") != current_element.end()) { + current_element["children"] = parse_level(current_element["inner"]); + current_element.erase("inner"); + } + if (current_element.find("children") != current_element.end()) { + for (auto& child: current_element["children"]) { + child = parse_tree(child); + } + } + return current_element; + } + + json parse(string input) { + return parse_tree({{"inner", input}})["children"]; + } + + + json parse_variable(string input, json data) { + // Json Raw Data + if ( json::accept(input) ) { + return json::parse(input); + } + + // TODO Implement filter and functions + + if (input[0] != '/') input.insert(0, "/"); + + json::json_pointer ptr(input); + json result = data[ptr]; + + if (result.is_null()) throw std::runtime_error("JSON pointer found no element."); + return result; + } + + bool parse_condition(string condition, json data) { + const std::regex regex_condition_equal("(.*) == (.*)"); + const std::regex regex_condition_greater("(.*) > (.*)"); + const std::regex regex_condition_less("(.*) < (.*)"); + const std::regex regex_condition_greater_equal("(.*) >= (.*)"); + const std::regex regex_condition_less_equal("(.*) <= (.*)"); + const std::regex regex_condition_different("(.*) != (.*)"); + const std::regex regex_condition_in("(.*) in (.*)"); + + std::smatch match_condition; + if (std::regex_match(condition, match_condition, regex_condition_equal)) { + json comp1 = parse_variable(match_condition[1].str(), data); + json comp2 = parse_variable(match_condition[2].str(), data); + return comp1 == comp2; + } else if (std::regex_match(condition, match_condition, regex_condition_greater)) { + json comp1 = parse_variable(match_condition[1].str(), data); + json comp2 = parse_variable(match_condition[2].str(), data); + return comp1 > comp2; + } else if (std::regex_match(condition, match_condition, regex_condition_less)) { + json comp1 = parse_variable(match_condition[1].str(), data); + json comp2 = parse_variable(match_condition[2].str(), data); + return comp1 < comp2; + } else if (std::regex_match(condition, match_condition, regex_condition_greater_equal)) { + json comp1 = parse_variable(match_condition[1].str(), data); + json comp2 = parse_variable(match_condition[2].str(), data); + return comp1 >= comp2; + } else if (std::regex_match(condition, match_condition, regex_condition_less_equal)) { + json comp1 = parse_variable(match_condition[1].str(), data); + json comp2 = parse_variable(match_condition[2].str(), data); + return comp1 <= comp2; + } else if (std::regex_match(condition, match_condition, regex_condition_different)) { + json comp1 = parse_variable(match_condition[1].str(), data); + json comp2 = parse_variable(match_condition[2].str(), data); + return comp1 != comp2; + } else if (std::regex_match(condition, match_condition, regex_condition_in)) { + json item = parse_variable(match_condition[1].str(), data); + json list = parse_variable(match_condition[2].str(), data); + return (std::find(list.begin(), list.end(), item) != list.end()); + } + + try { + return parse_variable(condition, data); + } + catch (...) { + return false; + } + } + + string render_json(json data) { + if (data.is_string()) { + return data; + } else { + std::stringstream ss; + ss << data; + return ss.str(); + } + } + + string render_tree(json input, json data, string path) { + string result = ""; + for (auto element: input) { + if (element["type"] == "string") { + result += element["text"]; + } + else if (element["type"] == "variable") { + json variable = parse_variable(element["command"], data); + result += render_json(variable); + } + else if (element["type"] == "include") { + result += render_template(path + element["filename"].get(), data); + } + else if (element["type"] == "loop") { + const std::regex regex_loop_list("for (\\w+) in (.+)"); + + string command = element["command"].get(); + std::smatch match_command; + if (std::regex_match(command, match_command, regex_loop_list)) { + string item_name = match_command[1].str(); + string list_name = match_command[2].str(); + + json list = parse_variable(list_name, data); + for (int i = 0; i < list.size(); i++) { + json data_loop = data; + data_loop[item_name] = list[i]; + data_loop["index"] = i; + data_loop["is_first"] = (i == 0); + data_loop["is_last"] = (i == list.size() - 1); + result += render_tree(element["children"], data_loop, path); + } + } + } + else if (element["type"] == "condition") { + const std::regex regex_condition("(if|else if|else) ?(.*)"); + + json branches = element["children"]; + for (auto branch: branches) { + + string command = branch["command"].get(); + std::smatch match_command; + if (std::regex_match(command, match_command, regex_condition)) { + string condition_type = match_command[1].str(); + string condition = match_command[2].str(); + + if (parse_condition(condition, data) || condition_type == "else") { + result += render_tree(branch["children"], data, path); + break; + } + } + } + } + } + return result; + } + + string render(string input, json data) { + return render(input, data, "./"); + } + + string render(string input, json data, string path) { + json parsed = parse(input); + return render_tree(parsed, data, path); + } + string render_template(string filename, json data) { string text = load_template(filename); string path = filename.substr(0, filename.find_last_of("/\\") + 1); // Include / itself diff --git a/src/json/LICENSE.MIT.txt b/src/json/LICENSE.MIT.txt deleted file mode 100644 index 00599af..0000000 --- a/src/json/LICENSE.MIT.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2013-2017 Niels Lohmann - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/test/src/test b/test/src/test index c29f7c4..edd1af8 100755 Binary files a/test/src/test and b/test/src/test differ diff --git a/test/src/test.cpp b/test/src/test.cpp index 6812056..ce8cd08 100644 --- a/test/src/test.cpp +++ b/test/src/test.cpp @@ -8,71 +8,215 @@ using Environment = inja::Environment; using json = nlohmann::json; -TEST_CASE("Variables") { +TEST_CASE("String functions") { + SECTION("Vector join") { + REQUIRE( inja::join_strings({"1", "2", "3"}, ",") == "1,2,3" ); + REQUIRE( inja::join_strings({"1", "2", "3", "4", "5"}, " ") == "1 2 3 4 5" ); + REQUIRE( inja::join_strings({}, " ") == "" ); + REQUIRE( inja::join_strings({"single"}, "---") == "single" ); + } + + SECTION("Basic search") { + inja::SearchMatch match = inja::search("lorem ipsum dolor it", std::regex("i(.*)m"), 0); + inja::SearchMatch match_position = inja::search("lorem ipsum dolor it", std::regex("i(.*)m"), 8); + + REQUIRE( match.found == true ); + REQUIRE( match.position == 6 ); + REQUIRE( match.length == 5 ); + REQUIRE( match.end_position == 11 ); + REQUIRE( match.outer == "ipsum" ); + REQUIRE( match.inner == "psu" ); + + REQUIRE( match_position.found == false ); + } + + SECTION("Vector search") { + std::vector regex_patterns = { "tras", "do(\\w*)or", "es(\\w*)as", "ip(\\w*)um" }; + inja::SearchMatch match = inja::search("lorem ipsum dolor amit estas tronum.", regex_patterns, 0); + + std::vector regex_patterns_rearranged = { "tras", "ip(\\w*)um", "do(\\w*)or", "es(\\w*)as" }; + inja::SearchMatch match_rearranged = inja::search("lorem ipsum dolor amit estas tronum.", regex_patterns_rearranged, 0); + + REQUIRE( match.regex_number == 3 ); + REQUIRE( match.outer == "ipsum" ); + REQUIRE( match.inner == "s" ); + + REQUIRE( match_rearranged.regex_number == 1 ); + REQUIRE( match_rearranged.outer == "ipsum" ); + REQUIRE( match_rearranged.inner == "s" ); + } +} + + +TEST_CASE("Parser") { Environment env = Environment(); + + SECTION("Basic") { + std::string test = "asdf"; + json result = {{{"type", "string"}, {"text", "asdf"}}}; + + REQUIRE( env.parse(test) == result ); + } + + SECTION("Variables") { + std::string test = "{{ name }}"; + json result = {{{"type", "variable"}, {"command", "name"}}}; + REQUIRE( env.parse(test) == result ); + + std::string test_combined = "Hello {{ name }}!"; + json result_combined = { + {{"type", "string"}, {"text", "Hello "}}, + {{"type", "variable"}, {"command", "name"}}, + {{"type", "string"}, {"text", "!"}} + }; + REQUIRE( env.parse(test_combined) == result_combined ); + + std::string test_multiple = "Hello {{ name }}! I come from {{ city }}."; + json result_multiple = { + {{"type", "string"}, {"text", "Hello "}}, + {{"type", "variable"}, {"command", "name"}}, + {{"type", "string"}, {"text", "! I come from "}}, + {{"type", "variable"}, {"command", "city"}}, + {{"type", "string"}, {"text", "."}} + }; + REQUIRE( env.parse(test_multiple) == result_multiple ); + } + + SECTION("Loops") { + std::string test = "open (% for e in list %)lorem(% endfor %) closing"; + json result = { + {{"type", "string"}, {"text", "open "}}, + {{"type", "loop"}, {"command", "for e in list"}, {"children", { + {{"type", "string"}, {"text", "lorem"}} + }}}, + {{"type", "string"}, {"text", " closing"}} + }; + + std::string test_nested = "(% for e in list %)(% for b in list2 %)lorem(% endfor %)(% endfor %)"; + json result_nested = { + {{"type", "loop"}, {"command", "for e in list"}, {"children", { + {{"type", "loop"}, {"command", "for b in list2"}, {"children", { + {{"type", "string"}, {"text", "lorem"}} + }}} + }}} + }; + + REQUIRE( env.parse(test) == result ); + REQUIRE( env.parse(test_nested) == result_nested ); + } + + SECTION("Conditionals") { + std::string test = "(% if true %)dfgh(% endif %)"; + json result = { + {{"type", "condition"}, {"children", { + {{"type", "condition_branch"}, {"command", "if true"}, {"children", { + {{"type", "string"}, {"text", "dfgh"}} + }}} + }}} + }; + + std::string test2 = "if: (% if maybe %)first if(% else if perhaps %)first else if(% else if sometimes %)second else if(% else %)test else(% endif %)"; + json result2 = { + {{"type", "string"}, {"text", "if: "}}, + {{"type", "condition"}, {"children", { + {{"type", "condition_branch"}, {"command", "if maybe"}, {"children", { + {{"type", "string"}, {"text", "first if"}} + }}}, + {{"type", "condition_branch"}, {"command", "else if perhaps"}, {"children", { + {{"type", "string"}, {"text", "first else if"}} + }}}, + {{"type", "condition_branch"}, {"command", "else if sometimes"}, {"children", { + {{"type", "string"}, {"text", "second else if"}} + }}}, + {{"type", "condition_branch"}, {"command", "else"}, {"children", { + {{"type", "string"}, {"text", "test else"}} + }}}, + }}} + }; + + REQUIRE( env.parse(test) == result ); + REQUIRE( env.parse(test2) == result2 ); + } + + json data; data["name"] = "Peter"; data["city"] = "Washington D.C."; + data["age"] = 29; data["names"] = {"Jeff", "Seb"}; data["brother"]["name"] = "Chris"; data["brother"]["daughters"] = {"Maria", "Helen"}; data["brother"]["daughter0"] = { { "name", "Maria" } }; - + SECTION("Variables from values") { - REQUIRE( env.get_variable_data("42", data) == 42 ); - REQUIRE( env.get_variable_data("3.1415", data) == 3.1415 ); - REQUIRE( env.get_variable_data("\"hello\"", data) == "hello" ); + REQUIRE( env.parse_variable("42", data) == 42 ); + REQUIRE( env.parse_variable("3.1415", data) == 3.1415 ); + REQUIRE( env.parse_variable("\"hello\"", data) == "hello" ); } - - SECTION("Variables from functions") { - REQUIRE( env.get_variable_data("range(3)", data) == std::vector({0, 1, 2}) ); - } - + SECTION("Variables from JSON data") { - REQUIRE( env.get_variable_data("name", data) == "Peter" ); - REQUIRE( env.get_variable_data("names/1", data) == "Seb" ); - REQUIRE( env.get_variable_data("brother/name", data) == "Chris" ); - REQUIRE( env.get_variable_data("brother/daughters/0", data) == "Maria" ); - REQUIRE_THROWS_WITH( env.get_variable_data("noelement", data), "JSON pointer found no element." ); - } - - SECTION("Variables should be rendered") { - REQUIRE( env.render("My name is...", data) == "My name is..." ); - REQUIRE( env.render("My name is {{ name }}.", data) == "My name is Peter." ); - REQUIRE( env.render("My name is {{name}}.", data) == "My name is Peter." ); - REQUIRE( env.render("My name is {{ name }}. I come from {{ city }}", data) == "My name is Peter. I come from Washington D.C." ); - REQUIRE( env.render("My name is {{ names/1 }}.", data) == "My name is Seb." ); - REQUIRE( env.render("My name is {{ brother/name }}.", data) == "My name is Chris." ); - REQUIRE( env.render("My name is {{ brother/daughter0/name }}.", data) == "My name is Maria." ); + REQUIRE( env.parse_variable("name", data) == "Peter" ); + REQUIRE( env.parse_variable("age", data) == 29 ); + REQUIRE( env.parse_variable("names/1", data) == "Seb" ); + REQUIRE( env.parse_variable("brother/name", data) == "Chris" ); + REQUIRE( env.parse_variable("brother/daughters/0", data) == "Maria" ); + REQUIRE_THROWS_WITH( env.parse_variable("noelement", data), "JSON pointer found no element." ); } } -TEST_CASE("Loops should be rendered") { +TEST_CASE("Render") { Environment env = Environment(); json data; - data["list"] = {"v1", "v2", "v3", "v4"}; + data["name"] = "Peter"; + data["city"] = "Brunswick"; + data["age"] = 29; + data["names"] = {"Jeff", "Seb"}; + data["brother"]["name"] = "Chris"; + data["brother"]["daughters"] = {"Maria", "Helen"}; + data["brother"]["daughter0"] = { { "name", "Maria" } }; + data["is_happy"] = true; + + SECTION("Basic") { + REQUIRE( env.render("Hello World!", data) == "Hello World!" ); + REQUIRE( env.render("", data, "../") == "" ); + } - REQUIRE( env.render("List: (% for entry in list %)a(% endfor %)", data) == "List: aaaa" ); - REQUIRE( env.render("List: (% for entry in list %){{ entry }}, (% endfor %)", data) == "List: v1, v2, v3, v4, " ); - REQUIRE( env.render("List: (% for i in range(4) %)a(% endfor %)", data) == "List: aaaa" ); + SECTION("Variables") { + REQUIRE( env.render("Hello {{ name }}!", data) == "Hello Peter!" ); + REQUIRE( env.render("{{ name }}", data) == "Peter" ); + REQUIRE( env.render("{{name}}", data) == "Peter" ); + REQUIRE( env.render("{{ name }} is {{ age }} years old.", data) == "Peter is 29 years old." ); + REQUIRE( env.render("Hello {{ name }}! I come from {{ city }}.", data) == "Hello Peter! I come from Brunswick." ); + REQUIRE( env.render("Hello {{ names/1 }}!", data) == "Hello Seb!" ); + REQUIRE( env.render("Hello {{ brother/name }}!", data) == "Hello Chris!" ); + REQUIRE( env.render("Hello {{ brother/daughter0/name }}!", data) == "Hello Maria!" ); + } + + SECTION("Comments") { + REQUIRE( env.render("Hello{# This is a comment #}!", data) == "Hello!" ); + REQUIRE( env.render("{# --- #Todo --- #}", data) == "" ); + } + + SECTION("Loops") { + REQUIRE( env.render("Hello (% for name in names %){{ name }} (% endfor %)!", data) == "Hello Jeff Seb !" ); + REQUIRE( env.render("Hello (% for name in names %){{ index }}: {{ name }}, (% endfor %)!", data) == "Hello 0: Jeff, 1: Seb, !" ); + } + + SECTION("Conditionals") { + REQUIRE( env.render("(% if is_happy %)Yeah!(% endif %)", data) == "Yeah!" ); + REQUIRE( env.render("(% if is_sad %)Yeah!(% endif %)", data) == "" ); + REQUIRE( env.render("(% if is_sad %)Yeah!(% else %)Nooo...(% endif %)", data) == "Nooo..." ); + REQUIRE( env.render("(% if age == 29 %)Right(% else %)Wrong(% endif %)", data) == "Right" ); + REQUIRE( env.render("(% if age > 29 %)Right(% else %)Wrong(% endif %)", data) == "Wrong" ); + REQUIRE( env.render("(% if age <= 29 %)Right(% else %)Wrong(% endif %)", data) == "Right" ); + REQUIRE( env.render("(% if age != 28 %)Right(% else %)Wrong(% endif %)", data) == "Right" ); + REQUIRE( env.render("(% if age >= 30 %)Right(% else %)Wrong(% endif %)", data) == "Wrong" ); + REQUIRE( env.render("(% if age in [28, 29, 30] %)True(% endif %)", data) == "True" ); + REQUIRE( env.render("(% if name in [\"Simon\", \"Tom\"] %)Test1(% else if name in [\"Peter\"] %)Test2(% else %)Test3(% endif %)", data) == "Test2" ); + } } -TEST_CASE("Conditionals should be rendered") { - Environment env = Environment(); - json data; - data["good"] = true; - data["bad"] = false; - data["a"] = 2; - - REQUIRE( env.render("(% if good %)a(% endif %)", data) == "a" ); - // REQUIRE( env.render("(% if good %)a(% endif %) (% if bad %)b(% endif %)", data) == "a " ); - // REQUIRE( env.render("(% if good %)one(% else %)two(% endif %)", data) == "one" ); - // REQUIRE( env.render("(% if bad %)one(% else %)two(% endif %)", data) == "two" ); - // REQUIRE( env.render("(% if a == 2 %)one(% else %)two(% endif %)", data) == "one" ); - // REQUIRE( env.render("(% if "b" in {"a", "b", "c"} %)one(% endif %)", data) == "one" ); -} - -TEST_CASE("Files should handled") { +TEST_CASE("Files") { Environment env = Environment(); json data; data["name"] = "Jeff";