diff --git a/CMakeLists.txt b/CMakeLists.txt index 7df3f54..054ab33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,14 +18,16 @@ set(CMAKE_CXX_STANDARD 11) set(INJA_SOURCE_DIR src/) set(INJA_HEADER_INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/dist) +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0") # debug, no optimisation +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") # enabling coverage + ## ## TESTS ## create and configure the unit test target ## if(BUILD_UNIT_TESTS) - enable_testing() - # include_directories(${NLOHMANN_JSON_SOURCE_DIR}) - add_subdirectory(test) + enable_testing() + add_subdirectory(test) endif() ## @@ -33,6 +35,6 @@ endif() ## install header files, generate and install cmake config files for find_package() ## install( - DIRECTORY ${INJA_SOURCE_DIR} - DESTINATION ${INJA_HEADER_INSTALL_DIR} + DIRECTORY ${INJA_SOURCE_DIR} + DESTINATION ${INJA_HEADER_INSTALL_DIR} ) diff --git a/README.md b/README.md index e259357..8e1daa4 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,7 @@ env.setLineStatements("##"); // Line statement (just an opener) ### Variables -Variables can be rendered using expressions within the `{{ ... }}` syntax. - +Variables can be rendered within the `{{ ... }}` expressions. ```c++ json data; data["neighbour"] = "Peter"; @@ -96,13 +95,12 @@ render("{{ guests/1 }}", data); // "Pierre" // Objects render("{{ time/start }} to {{ time/end }}pm"); // "16 to 22pm" ``` - In general, the variables can be fetched using the [JSON Pointer](https://tools.ietf.org/html/rfc6901) syntax. For convenience, the leading `/` can be ommited. If no variable is found, valid JSON is printed directly, otherwise an error is thrown. ### Statements -Statements can be written with the `(% ... %)` syntax. The most important statements are loops, conditions and file includes.All statements can be nested. +Statements can be written with the `{% ... %}` syntax. The most important statements are loops, conditions and file includes. All statements can be nested. #### Loops @@ -118,34 +116,35 @@ render(R"(Guest List: 2: Pierre 3: Tom */ ``` - In a loop, the special variables `number index`, `number index1`, `bool is_first` and `bool is_last` are available. #### Conditions -Conditions support if, else if and else statements, they can be nested. Following conditions for example: -``` +Conditions support if, else if and else statements. Following conditions for example: +```c++ // Standard comparisons with variable -{% if time/hour >= 18 %}…{% endif %} +render("{% if time/hour >= 18 %}…{% endif %}", data); // True // Variable in list -{% if neighbour in guests %}…{% endif %} +render("{% if neighbour in guests %}…{% endif %}", data); // True // Logical operations -{% if guest_count < 5 and all_tired %}That looks like the end.{% endif %} +render("{% if guest_count < 5 and all_tired %}…{% endif %}", data); // True -// And finally -{% if not guest_count %}Jep, that's it.{% endif %} +// Negations +render("{% if not guest_count %}…{% endif %}", data); // True ``` #### Includes -Include other files like `{% include "footer.html" %}`. Relative from file. +Include other files, relative from the current file location. +``` +{% include "footer.html" %} +``` ### Comments Comments can be written with the `{# ... #}` syntax. - ```c++ render("Hello{# Todo #}!", data); // "Hello!" ``` diff --git a/src/inja.hpp b/src/inja.hpp index 8331e51..f3acff7 100644 --- a/src/inja.hpp +++ b/src/inja.hpp @@ -15,184 +15,138 @@ namespace inja { using json = nlohmann::json; -using string = std::string; -inline string join_strings(std::vector vector, string delimiter) { - std::stringstream ss; - for (size_t i = 0; i < vector.size(); ++i) - { - if (i != 0) { ss << delimiter; } - ss << vector[i]; - } - return ss.str(); +template +inline std::vector get_values(std::map map) { + std::vector result; + for (auto const element: map) { result.push_back(element.second); } + return result; } -/* class Match: public std::smatch { - size_t offset; +class Regex: public std::regex { + std::string pattern_; public: - Match() {} - Match(std::smatch match, size_t offset): std::smatch(match), offset(offset) { } + Regex(): std::regex() {} + Regex(std::string pattern): std::regex(pattern), pattern_(pattern) { } - size_t position() { return offset + std::smatch::position(); } + std::string pattern() { return pattern_; } +}; + + +class Match: public std::smatch { + size_t offset_ = 0; + int group_offset_ = 0; + Regex regex_; + unsigned int regex_number_ = 0; + +public: + Match(): std::smatch() { } + Match(size_t offset): std::smatch(), offset_(offset) { } + Match(size_t offset, Regex regex): std::smatch(), offset_(offset), regex_(regex) { } + + void setGroupOffset(int group_offset) { group_offset_ = group_offset; } + void setRegex(Regex regex) { regex_ = regex; } + void setRegexNumber(unsigned int regex_number) { regex_number_ = regex_number; } + + size_t position() { return offset_ + std::smatch::position(); } size_t end_position() { return position() + length(); } - size_t found() { return not empty(); } - string outer() { return str(0); } - string inner() { return str(1); } + bool found() { return not empty(); } + std::string str() { return str(0); } + std::string str(int i) { return std::smatch::str(i + group_offset_); } + Regex regex() { return regex_; } + unsigned int regex_number() { return regex_number_; } }; -class MatchVector: public Match { - unsigned int matched_regex_position, matched_inner_group_position; - std::vector regex_patterns; - -public: - MatchVector(): Match() { } - MatchVector(std::smatch match, size_t offset, unsigned int matched_regex_position, unsigned int matched_inner_group_position, std::vector regex_patterns): Match(match, offset), matched_regex_position(matched_regex_position), matched_inner_group_position(matched_inner_group_position), regex_patterns(regex_patterns) { } - - string inner() { return str(matched_inner_group_position); } - std::regex matched_regex() { return std::regex(regex_patterns[matched_regex_position]); } -}; - -class MatchClosed: public Match { - string _outer, _inner; +class MatchClosed { public: Match open_match, close_match; + std::string inner, outer; - MatchClosed(): Match() { } - MatchClosed(string input, Match open_match, Match close_match): Match(), open_match(open_match), close_match(close_match) { - _outer = input.substr(open_match.position(), close_match.end_position() - open_match.position()); - _inner = input.substr(open_match.end_position(), close_match.position() - open_match.end_position()); + 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()); } size_t position() { return open_match.position(); } - size_t length() { return close_match.end_position() - open_match.position(); } size_t end_position() { return close_match.end_position(); } + int length() { return close_match.end_position() - open_match.position(); } bool found() { return open_match.found() and close_match.found(); } - string outer() { return _outer; } - string inner() { return _inner; } - string prefix() { return open_match.prefix(); } - string suffix() { return open_match.suffix(); } -}; */ - - - - - -struct SearchMatch { - SearchMatch() { } - - SearchMatch(std::smatch match, size_t offset): match(match), offset(offset) { - position = offset + match.position(); - length = match.length(); - end_position = position + length; - found = not match.empty(); - outer = match.str(0); - inner = match.str(1); - prefix = match.prefix(); - suffix = match.suffix(); - } - - std::smatch match; - size_t offset; - - string outer, inner, prefix, suffix; - size_t position, end_position; - int length; - bool found = false; + std::string prefix() { return open_match.prefix(); } + std::string suffix() { return close_match.suffix(); } }; -struct SearchMatchVector: public SearchMatch { - SearchMatchVector(): SearchMatch() { } - SearchMatchVector(std::smatch match, size_t offset, int inner_group, int regex_number, std::vector regex_patterns): SearchMatch(match, offset), regex_number(regex_number) { - inner = match.str(inner_group); - if (regex_number >= 0) { - regex_delimiter = std::regex( regex_patterns.at(regex_number) ); - } - } +inline Match search(const std::string& input, Regex regex, size_t position) { + if (position >= input.length()) { return Match(); } - int regex_number = -1; - std::regex regex_delimiter; -}; - -struct SearchClosedMatch: public SearchMatch { - SearchClosedMatch(): SearchMatch() { } - - SearchClosedMatch(string input, SearchMatch open_match, SearchMatch close_match): SearchMatch(), 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; - } - - SearchMatch open_match, close_match; -}; - -inline SearchMatch search(string input, std::regex regex, size_t position) { - if (position >= input.length()) { return SearchMatch(); } - - std::smatch match; + Match match{position, regex}; std::regex_search(input.cbegin() + position, input.cend(), match, regex); - return SearchMatch(match, position); + return match; } -inline SearchMatchVector search(string input, std::vector regex_patterns, size_t position) { - string regex_pattern = "(" + join_strings(regex_patterns, ")|(") + ")"; +inline Match search(const std::string& input, std::vector regexes, size_t position) { + // Regex or join + std::stringstream ss; + for (size_t i = 0; i < regexes.size(); ++i) + { + if (i != 0) { ss << ")|("; } + ss << regexes[i].pattern(); + } + Regex regex{"(" + ss.str() + ")"}; - if (position >= input.length()) { return SearchMatchVector(); } - - std::smatch match; - std::regex_search(input.cbegin() + position, input.cend(), match, std::regex(regex_pattern)); - SearchMatch search_match = SearchMatch(match, position); - // SearchMatch search_match = search(input, std::regex(regex_pattern), position); - if (not search_match.found) { return SearchMatchVector(); } + Match search_match = search(input, regex, position); + if (not search_match.found()) { return Match(); } // Vector of id vs groups + // 0: 1, 1: 2, 2: 3, 3: 2 + // 0 1 1 2 2 2 3 3 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++) { + for (int i = 0; i < regexes.size(); i++) { + for (int j = 0; j < regexes[i].mark_count() + 1; j++) { regex_mark_counts.push_back(i); } } int number_regex = 0, number_inner = 1; - for (int i = 1; i < search_match.match.size(); i++) { - if (search_match.match.length(i) > 0) { - number_inner = i + 1; + for (int i = 1; i < search_match.size(); i++) { + if (search_match.length(i) > 0) { + number_inner = i; number_regex = regex_mark_counts[i]; break; } } - return SearchMatchVector(search_match.match, position, number_inner, number_regex, regex_patterns); + search_match.setGroupOffset(number_inner); + search_match.setRegexNumber(number_regex); + search_match.setRegex(regexes[number_regex]); + return search_match; } -inline SearchClosedMatch search_closed_match_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) { +inline MatchClosed search_closed_match_on_level(const std::string& input, Regex regex_statement, Regex regex_level_up, Regex regex_level_down, Regex regex_search, Match 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; + size_t current_position = open_match.end_position(); + Match match_delimiter = search(input, regex_statement, current_position); + while (match_delimiter.found()) { + current_position = match_delimiter.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; } + std::string inner = match_delimiter.str(1); + if (std::regex_match(inner, regex_search) and level == 0) { break; } + if (std::regex_match(inner, regex_level_up)) { level += 1; } + else if (std::regex_match(inner, regex_level_down)) { level -= 1; } - statement_match = search(input, regex_statement, current_position); + match_delimiter = search(input, regex_statement, current_position); } - return SearchClosedMatch(input, open_match, statement_match); + return MatchClosed(input, open_match, match_delimiter); } -inline SearchClosedMatch search_closed_match(string input, std::regex regex_statement, std::regex regex_open, std::regex regex_close, SearchMatch open_match) { +inline MatchClosed search_closed_match(std::string input, Regex regex_statement, Regex regex_open, Regex regex_close, Match open_match) { return search_closed_match_on_level(input, regex_statement, regex_open, regex_close, regex_close, open_match); } @@ -206,8 +160,6 @@ public: Comment }; - // std::map regex_pattern_map; - enum class Type { String, Loop, @@ -217,118 +169,116 @@ public: Comment, Variable }; -}; + std::map regex_map_delimiters = { + {Delimiter::Statement, Regex{"\\{\\%\\s*(.+?)\\s*\\%\\}"}}, + {Delimiter::LineStatement, Regex{"(?:^|\\n)##\\s*(.+)\\s*"}}, + {Delimiter::Expression, Regex{"\\{\\{\\s*(.+?)\\s*\\}\\}"}}, + {Delimiter::Comment, Regex{"\\{#\\s*(.*?)\\s*#\\}"}} + }; -class Environment { - std::regex regex_statement; - std::regex regex_line_statement; - std::regex regex_expression; - std::regex regex_comment; - std::vector regex_pattern_delimiters; + const Regex regex_loop_open{"for (.*)"}; + const Regex regex_loop_close{"endfor"}; - string global_path; + const Regex regex_include{"include \"(.*)\""}; + const Regex regex_condition_open{"if (.*)"}; + const Regex regex_condition_else_if{"else if (.*)"}; + const Regex regex_condition_else{"else"}; + const Regex regex_condition_close{"endif"}; -public: - Environment(): Environment("./") { } + const Regex regex_condition_not{"not (.+)"}; + const Regex regex_condition_and{"(.+) and (.+)"}; + const Regex regex_condition_or{"(.+) or (.+)"}; + const Regex regex_condition_in{"(.+) in (.+)"}; + const Regex regex_condition_equal{"(.+) == (.+)"}; + const Regex regex_condition_greater{"(.+) > (.+)"}; + const Regex regex_condition_less{"(.+) < (.+)"}; + const Regex regex_condition_greater_equal{"(.+) >= (.+)"}; + const Regex regex_condition_less_equal{"(.+) <= (.+)"}; + const Regex regex_condition_different{"(.+) != (.+)"}; - Environment(string global_path): global_path(global_path) { - const string regex_pattern_statement = "\\(\\%\\s*(.+?)\\s*\\%\\)"; - const string regex_pattern_line_statement = "(?:^|\\n)##\\s*(.+)\\s*"; - const string regex_pattern_expression = "\\{\\{\\s*(.+?)\\s*\\}\\}"; - const string regex_pattern_comment = "\\{#\\s*(.*?)\\s*#\\}"; + const Regex regex_function_range{"range\\(\\s*(.*?)\\s*\\)"}; + const Regex regex_function_upper{"upper\\(\\s*(.*?)\\s*\\)"}; + const Regex regex_function_lower{"lower\\(\\s*(.*?)\\s*\\)"}; - // Parser parser; - // parser.regex_pattern_map[Parser::Delimiter::Statement] = "\\(\\%\\s*(.+?)\\s*\\%\\)"; + Parser() { } - 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_line_statement, regex_pattern_expression, regex_pattern_comment }; // Order of Parser::Delimiter enum + json element(Type type, json element_data) { + element_data["type"] = type; + return element_data; } - - json parse_level(string input) { + json parse_level(std::string input) { json result; + std::vector regex_delimiters = get_values(regex_map_delimiters); + size_t current_position = 0; - SearchMatchVector statement_match = search(input, regex_pattern_delimiters, current_position); - while (statement_match.found) { - current_position = statement_match.end_position; - if (not statement_match.prefix.empty()) { - result += {{"type", Parser::Type::String}, {"text", statement_match.prefix}}; + Match match_delimiter = search(input, regex_delimiters, current_position); + while (match_delimiter.found()) { + current_position = match_delimiter.end_position(); + std::string string_prefix = match_delimiter.prefix(); + if (not string_prefix.empty()) { + result += element(Type::String, {{"text", string_prefix}}); } - switch ( static_cast(statement_match.regex_number) ) { - case Parser::Delimiter::Statement: - case Parser::Delimiter::LineStatement: { - const std::regex regex_loop_open("for (.*)"); - const std::regex regex_loop_close("endfor"); + switch ( static_cast(match_delimiter.regex_number()) ) { + case Delimiter::Statement: + case Delimiter::LineStatement: { - 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; + Match inner_match_delimiter; // Loop - if (std::regex_match(statement_match.inner, inner_statement_match, regex_loop_open)) { - SearchClosedMatch loop_match = search_closed_match(input, statement_match.regex_delimiter, regex_loop_open, regex_loop_close, statement_match); + if (std::regex_match(match_delimiter.str(1), inner_match_delimiter, regex_loop_open)) { + MatchClosed loop_match = search_closed_match(input, match_delimiter.regex(), regex_loop_open, regex_loop_close, match_delimiter); - current_position = loop_match.end_position; - string loop_command = inner_statement_match.str(0); - result += {{"type", Parser::Type::Loop}, {"command", loop_command}, {"inner", loop_match.inner}}; + 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}}); } // Include - else if (std::regex_match(statement_match.inner, inner_statement_match, regex_include)) { - string include_command = inner_statement_match.str(0); - string filename = inner_statement_match.str(1); - result += {{"type", Parser::Type::Include}, {"filename", filename}}; + else if (std::regex_match(match_delimiter.str(1), inner_match_delimiter, regex_include)) { + std::string filename = inner_match_delimiter.str(1); + result += element(Type::Include, {{"filename", filename}}); } // Condition - else if (std::regex_match(statement_match.inner, inner_statement_match, regex_condition_open)) { - string if_command = inner_statement_match.str(0); - json condition_result = {{"type", Parser::Type::Condition}, {"children", json::array()}}; + else if (std::regex_match(match_delimiter.str(1), inner_match_delimiter, regex_condition_open)) { + json condition_result = element(Parser::Type::Condition, {{"children", json::array()}}); + Match condition_match = match_delimiter; - SearchMatch condition_match = statement_match; - - SearchClosedMatch else_if_match = search_closed_match_on_level(input, statement_match.regex_delimiter, regex_condition_open, regex_condition_close, regex_condition_else_if, condition_match); - while (else_if_match.found) { + MatchClosed else_if_match = search_closed_match_on_level(input, match_delimiter.regex(), 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"] += {{"type", Parser::Type::ConditionBranch}, {"command", else_if_match.open_match.inner}, {"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, regex_statement, regex_condition_open, regex_condition_close, regex_condition_else_if, condition_match); + else_if_match = search_closed_match_on_level(input, match_delimiter.regex(), regex_condition_open, regex_condition_close, regex_condition_else_if, condition_match); } - SearchClosedMatch else_match = search_closed_match_on_level(input, statement_match.regex_delimiter, regex_condition_open, regex_condition_close, regex_condition_else, condition_match); - if (else_match.found) { + MatchClosed else_match = search_closed_match_on_level(input, match_delimiter.regex(), regex_condition_open, regex_condition_close, regex_condition_else, condition_match); + if (else_match.found()) { condition_match = else_match.close_match; - condition_result["children"] += {{"type", Parser::Type::ConditionBranch}, {"command", else_match.open_match.inner}, {"inner", else_match.inner}}; + condition_result["children"] += element(Type::ConditionBranch, {{"command", else_match.open_match.str(1)}, {"inner", else_match.inner}}); } - SearchClosedMatch last_if_match = search_closed_match(input, statement_match.regex_delimiter, regex_condition_open, regex_condition_close, condition_match); + MatchClosed last_if_match = search_closed_match(input, match_delimiter.regex(), regex_condition_open, regex_condition_close, condition_match); - condition_result["children"] += {{"type", Parser::Type::ConditionBranch}, {"command", last_if_match.open_match.inner}, {"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; + current_position = last_if_match.end_position(); result += condition_result; } break; } - case Parser::Delimiter::Expression: { - result += {{"type", Parser::Type::Variable}, {"command", statement_match.inner}}; + case Delimiter::Expression: { + result += element(Type::Variable, {{"command", match_delimiter.str(1)}}); break; } - case Parser::Delimiter::Comment: { - result += {{"type", Parser::Type::Comment}, {"text", statement_match.inner}}; + case Delimiter::Comment: { + result += element(Type::Comment, {{"text", match_delimiter.str(1)}}); break; } default: { @@ -336,10 +286,10 @@ public: } } - statement_match = search(input, regex_pattern_delimiters, current_position); + match_delimiter = search(input, regex_delimiters, current_position); } if (current_position < input.length()) { - result += {{"type", Parser::Type::String}, {"text", input.substr(current_position)}}; + result += element(Parser::Type::String, {{"text", input.substr(current_position)}}); } return result; @@ -358,23 +308,39 @@ public: return current_element; } - json parse(string input) { + json parse(std::string input) { return parse_tree({{"inner", input}})["children"]; } +}; - json parse_variable(string input, json data) { + +class Environment { + std::string global_path; + + Parser parser; + + +public: + Environment(): Environment("./") { } + Environment(std::string global_path): global_path(global_path), parser() { } + + void setExpression(std::string open, std::string close) { + 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 parse_variable(string input, json data, bool throw_error) { + json parse_variable(std::string input, json data, bool throw_error) { // Json Raw Data if ( json::accept(input) ) { return json::parse(input); } - // TODO Implement filter and functions + // TODO Implement functions + if (input[0] != '/') { input.insert(0, "/"); } - json::json_pointer ptr(input); json result = data[ptr]; @@ -382,73 +348,62 @@ public: return result; } - bool parse_condition(string condition, json data) { - const std::regex regex_condition_not("not (.+)"); - const std::regex regex_condition_and("(.+) and (.+)"); - const std::regex regex_condition_or("(.+) or (.+)"); - 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_not)) { + bool parse_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); } - else if (std::regex_match(condition, match_condition, regex_condition_and)) { + 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)); } - else if (std::regex_match(condition, match_condition, regex_condition_or)) { + 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)); } - else if (std::regex_match(condition, match_condition, regex_condition_equal)) { + 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); + 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); return comp1 == comp2; } - else if (std::regex_match(condition, match_condition, regex_condition_greater)) { + 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); return comp1 > comp2; } - else if (std::regex_match(condition, match_condition, regex_condition_less)) { + 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); return comp1 < comp2; } - else if (std::regex_match(condition, match_condition, regex_condition_greater_equal)) { + 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); return comp1 >= comp2; } - else if (std::regex_match(condition, match_condition, regex_condition_less_equal)) { + 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); return comp1 <= comp2; } - else if (std::regex_match(condition, match_condition, regex_condition_different)) { + 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); return comp1 != comp2; } - else if (std::regex_match(condition, match_condition, regex_condition_in)) { - json item = parse_variable(match_condition.str(1), data); - json list = parse_variable(match_condition.str(2), data); - return (std::find(list.begin(), list.end(), item) != list.end()); - } json var = parse_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 (var != ""); } + else if (var.is_string()) { return not var.empty(); } return false; } - string render_json(json data) { + std::string render_json(json data) { if (data.is_string()) { return data; } std::stringstream ss; @@ -456,12 +411,12 @@ public: return ss.str(); } - string render_tree(json input, json data, string path) { - string result = ""; + std::string render_tree(json input, json data, std::string path) { + std::string result = ""; for (auto element: input) { switch ( static_cast(element["type"]) ) { case Parser::Type::String: { - result += render_json(element["text"]); + result += element["text"].get(); break; } case Parser::Type::Variable: { @@ -470,17 +425,17 @@ public: break; } case Parser::Type::Include: { - result += render_template(path + element["filename"].get(), data); + result += render_template(path + element["filename"].get(), data); break; } case Parser::Type::Loop: { - const std::regex regex_loop_list("for (\\w+) in (.+)"); + const Regex regex_loop_list{"for (\\w+) in (.+)"}; - string command = element["command"].get(); - std::smatch match_command; + std::string command = element["command"].get(); + Match match_command; if (std::regex_match(command, match_command, regex_loop_list)) { - string item_name = match_command.str(1); - string list_name = match_command.str(2); + std::string item_name = match_command.str(1); + std::string list_name = match_command.str(2); json list = parse_variable(list_name, data); for (int i = 0; i < list.size(); i++) { @@ -499,14 +454,14 @@ public: break; } case Parser::Type::Condition: { - const std::regex regex_condition("(if|else if|else) ?(.*)"); + const Regex regex_condition{"(if|else if|else) ?(.*)"}; for (auto branch: element["children"]) { - string command = branch["command"].get(); - std::smatch match_command; + std::string command = branch["command"].get(); + Match match_command; if (std::regex_match(command, match_command, regex_condition)) { - string condition_type = match_command.str(1); - string condition = match_command.str(2); + std::string condition_type = match_command.str(1); + std::string condition = match_command.str(2); if (parse_condition(condition, data) || condition_type == "else") { result += render_tree(branch["children"], data, path); @@ -530,34 +485,34 @@ public: return result; } - string render(string input, json data, string path) { - json parsed = parse(input); + std::string render(std::string input, json data, std::string path) { + json parsed = parser.parse(input); return render_tree(parsed, data, path); } - string render(string input, json data) { + std::string render(std::string input, json data) { return render(input, data, "./"); } - string render_template(string filename, json data) { - string text = load_file(filename); - string path = filename.substr(0, filename.find_last_of("/\\") + 1); + std::string render_template(std::string filename, json data) { + std::string text = load_file(filename); + std::string path = filename.substr(0, filename.find_last_of("/\\") + 1); return render(text, data, path); } - string render_template_with_json_file(string filename, string filename_data) { + std::string render_template_with_json_file(std::string filename, std::string filename_data) { json data = load_json(filename_data); return render_template(filename, data); } - string load_file(string filename) { + std::string load_file(std::string filename) { std::ifstream file(global_path + filename); - string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); return text; } - json load_json(string filename) { + json load_json(std::string filename) { std::ifstream file(global_path + filename); json j; file >> j; @@ -565,7 +520,7 @@ public: } }; -inline string render(string input, json data) { +inline std::string render(std::string input, json data) { return Environment().render(input, data); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ea9fce3..a3c341a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -9,7 +9,6 @@ set(UNITTEST_TARGET_NAME "inja_unit") file(GLOB TEST_SOURCES "src/*.cpp") add_executable(${UNITTEST_TARGET_NAME} ${TEST_SOURCES}) - target_link_libraries(${UNITTEST_TARGET_NAME} Catch) target_include_directories(${UNITTEST_TARGET_NAME} PRIVATE "../src" "thirdparty/json") @@ -19,4 +18,4 @@ target_include_directories(${UNITTEST_TARGET_NAME} PRIVATE "../src" "thirdparty/ add_test(NAME "${UNITTEST_TARGET_NAME}_default" COMMAND ${UNITTEST_TARGET_NAME} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/build -) \ No newline at end of file +) diff --git a/test/data/include.txt b/test/data/include.txt index 01abe5c..dbcae63 100644 --- a/test/data/include.txt +++ b/test/data/include.txt @@ -1 +1 @@ -Answer: (% include "simple.txt" %) \ No newline at end of file +Answer: {% include "simple.txt" %} \ No newline at end of file diff --git a/test/data/nested/template.txt b/test/data/nested/template.txt index fd264e9..254dce2 100644 --- a/test/data/nested/template.txt +++ b/test/data/nested/template.txt @@ -1,3 +1,3 @@ -(% for x in xarray %)(% for y in yarray %) +{% for x in xarray %}{% for y in yarray %} {{x}}-{{y}} -(% endfor %)(% endfor %) \ No newline at end of file +{% endfor %}{% endfor %} \ No newline at end of file diff --git a/test/src/unit-files.cpp b/test/src/unit-files.cpp index 62aa628..62616e2 100644 --- a/test/src/unit-files.cpp +++ b/test/src/unit-files.cpp @@ -3,12 +3,11 @@ #include "inja.hpp" -using Environment = inja::Environment; using json = nlohmann::json; TEST_CASE("Files handling") { - Environment env = Environment(); + inja::Environment env = inja::Environment(); json data; data["name"] = "Jeff"; @@ -26,7 +25,7 @@ TEST_CASE("Files handling") { } TEST_CASE("Complete files") { - Environment env = Environment("../test/data/"); + inja::Environment env = inja::Environment("../test/data/"); for (std::string test_name : {"simple-file", "nested", "nested-line"}) { SECTION(test_name) { diff --git a/test/src/unit-parser.cpp b/test/src/unit-parser.cpp index 09d444e..8e37f84 100644 --- a/test/src/unit-parser.cpp +++ b/test/src/unit-parser.cpp @@ -3,30 +3,29 @@ #include "inja.hpp" -using Environment = inja::Environment; using json = nlohmann::json; using Type = inja::Parser::Type; TEST_CASE("Parse structure") { - Environment env = Environment(); + inja::Parser parser = inja::Parser(); SECTION("Basic string") { std::string test = "lorem ipsum"; json result = {{{"type", Type::String}, {"text", "lorem ipsum"}}}; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("Empty string") { std::string test = ""; json result = {}; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("Variable") { std::string test = "{{ name }}"; json result = {{{"type", Type::Variable}, {"command", "name"}}}; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("Combined string and variables") { @@ -36,7 +35,7 @@ TEST_CASE("Parse structure") { {{"type", Type::Variable}, {"command", "name"}}, {{"type", Type::String}, {"text", "!"}} }; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("Multiple variables") { @@ -48,11 +47,11 @@ TEST_CASE("Parse structure") { {{"type", Type::Variable}, {"command", "city"}}, {{"type", Type::String}, {"text", "."}} }; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("Loops") { - std::string test = "open (% for e in list %)lorem(% endfor %) closing"; + std::string test = "open {% for e in list %}lorem{% endfor %} closing"; json result = { {{"type", Type::String}, {"text", "open "}}, {{"type", Type::Loop}, {"command", "for e in list"}, {"children", { @@ -60,11 +59,11 @@ TEST_CASE("Parse structure") { }}}, {{"type", Type::String}, {"text", " closing"}} }; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("Nested loops") { - std::string test = "(% for e in list %)(% for b in list2 %)lorem(% endfor %)(% endfor %)"; + std::string test = "{% for e in list %}{% for b in list2 %}lorem{% endfor %}{% endfor %}"; json result = { {{"type", Type::Loop}, {"command", "for e in list"}, {"children", { {{"type", Type::Loop}, {"command", "for b in list2"}, {"children", { @@ -72,11 +71,11 @@ TEST_CASE("Parse structure") { }}} }}} }; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("Basic conditional") { - std::string test = "(% if true %)dfgh(% endif %)"; + std::string test = "{% if true %}dfgh{% endif %}"; json result = { {{"type", Type::Condition}, {"children", { {{"type", Type::ConditionBranch}, {"command", "if true"}, {"children", { @@ -84,11 +83,11 @@ TEST_CASE("Parse structure") { }}} }}} }; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("If/else if/else conditional") { - std::string test = "if: (% if maybe %)first if(% else if perhaps %)first else if(% else if sometimes %)second else if(% else %)test else(% endif %)"; + std::string test = "if: {% if maybe %}first if{% else if perhaps %}first else if{% else if sometimes %}second else if{% else %}test else{% endif %}"; json result = { {{"type", Type::String}, {"text", "if: "}}, {{"type", Type::Condition}, {"children", { @@ -106,13 +105,13 @@ TEST_CASE("Parse structure") { }}}, }}} }; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("Comments") { std::string test = "{# lorem ipsum #}"; json result = {{{"type", Type::Comment}, {"text", "lorem ipsum"}}}; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } SECTION("Line Statements") { @@ -126,12 +125,12 @@ lorem ipsum }}} }}} }; - CHECK( env.parse(test) == result ); + CHECK( parser.parse(test) == result ); } } TEST_CASE("Parse json") { - Environment env = Environment(); + inja::Environment env = inja::Environment(); json data; data["name"] = "Peter"; @@ -159,7 +158,7 @@ TEST_CASE("Parse json") { } TEST_CASE("Parse conditions") { - Environment env = Environment(); + inja::Environment env = inja::Environment(); json data; data["age"] = 29; diff --git a/test/src/unit-renderer.cpp b/test/src/unit-renderer.cpp index 4e1ef9d..83fa702 100644 --- a/test/src/unit-renderer.cpp +++ b/test/src/unit-renderer.cpp @@ -41,20 +41,20 @@ TEST_CASE("Renderer") { } SECTION("Loops") { - CHECK( env.render("Hello (% for name in names %){{ name }} (% endfor %)!", data) == "Hello Jeff Seb !" ); - CHECK( env.render("Hello (% for name in names %){{ index }}: {{ name }}, (% endfor %)!", data) == "Hello 0: Jeff, 1: Seb, !" ); + CHECK( env.render("Hello {% for name in names %}{{ name }} {% endfor %}!", data) == "Hello Jeff Seb !" ); + CHECK( env.render("Hello {% for name in names %}{{ index }}: {{ name }}, {% endfor %}!", data) == "Hello 0: Jeff, 1: Seb, !" ); } SECTION("Conditionals") { - CHECK( env.render("(% if is_happy %)Yeah!(% endif %)", data) == "Yeah!" ); - CHECK( env.render("(% if is_sad %)Yeah!(% endif %)", data) == "" ); - CHECK( env.render("(% if is_sad %)Yeah!(% else %)Nooo...(% endif %)", data) == "Nooo..." ); - CHECK( env.render("(% if age == 29 %)Right(% else %)Wrong(% endif %)", data) == "Right" ); - CHECK( env.render("(% if age > 29 %)Right(% else %)Wrong(% endif %)", data) == "Wrong" ); - CHECK( env.render("(% if age <= 29 %)Right(% else %)Wrong(% endif %)", data) == "Right" ); - CHECK( env.render("(% if age != 28 %)Right(% else %)Wrong(% endif %)", data) == "Right" ); - CHECK( env.render("(% if age >= 30 %)Right(% else %)Wrong(% endif %)", data) == "Wrong" ); - CHECK( env.render("(% if age in [28, 29, 30] %)True(% endif %)", data) == "True" ); - // CHECK( env.render(R"((% if name in ["Simon", "Tom"] %)Test1(% else if name in ["Peter"] %)Test2(% else %)Test3(% endif %))", data) == "Test2" ); + CHECK( env.render("{% if is_happy %}Yeah!{% endif %}", data) == "Yeah!" ); + CHECK( env.render("{% if is_sad %}Yeah!{% endif %}", data) == "" ); + CHECK( env.render("{% if is_sad %}Yeah!{% else %}Nooo...{% endif %}", data) == "Nooo..." ); + CHECK( env.render("{% if age == 29 %}Right{% else %}Wrong{% endif %}", data) == "Right" ); + CHECK( env.render("{% if age > 29 %}Right{% else %}Wrong{% endif %}", data) == "Wrong" ); + CHECK( env.render("{% if age <= 29 %}Right{% else %}Wrong{% endif %}", data) == "Right" ); + CHECK( env.render("{% if age != 28 %}Right{% else %}Wrong{% endif %}", data) == "Right" ); + CHECK( env.render("{% if age >= 30 %}Right{% else %}Wrong{% endif %}", data) == "Wrong" ); + CHECK( env.render("{% if age in [28, 29, 30] %}True{% endif %}", data) == "True" ); + // CHECK( env.render(R"({% if name in ["Simon", "Tom"] %}Test1{% else if name in ["Peter"] %}Test2{% else %}Test3{% endif %})", data) == "Test2" ); } } diff --git a/test/src/unit-string-helper.cpp b/test/src/unit-string-helper.cpp index 256499c..9ab6b01 100644 --- a/test/src/unit-string-helper.cpp +++ b/test/src/unit-string-helper.cpp @@ -3,31 +3,25 @@ #include "inja.hpp" -TEST_CASE("String vector join function") { - CHECK( inja::join_strings({"1", "2", "3"}, ",") == "1,2,3" ); - CHECK( inja::join_strings({"1", "2", "3", "4", "5"}, " ") == "1 2 3 4 5" ); - CHECK( inja::join_strings({}, " ") == "" ); - CHECK( inja::join_strings({"single"}, "---") == "single" ); -} TEST_CASE("Basic search in string") { std::string input = "lorem ipsum dolor it"; - std::regex regex("i(.*)m"); + inja::Regex regex("i(.*)m"); SECTION("Basic search from start") { - inja::SearchMatch match = inja::search(input, regex, 0); - CHECK( match.found == true ); - CHECK( match.position == 6 ); - CHECK( match.length == 5 ); - CHECK( match.end_position == 11 ); - CHECK( match.outer == "ipsum" ); - CHECK( match.inner == "psu" ); + inja::Match match = inja::search(input, regex, 0); + CHECK( match.found() == true ); + CHECK( match.position() == 6 ); + CHECK( match.length() == 5 ); + CHECK( match.end_position() == 11 ); + CHECK( match.str() == "ipsum" ); + CHECK( match.str(1) == "psu" ); } SECTION("Basic search from position") { - inja::SearchMatch match = inja::search(input, regex, 8); - CHECK( match.found == false ); - CHECK( match.length == 0 ); + inja::Match match = inja::search(input, regex, 8); + CHECK( match.found() == false ); + CHECK( match.length() == 0 ); } } @@ -35,56 +29,56 @@ TEST_CASE("Search in string with multiple possible regexes") { std::string input = "lorem ipsum dolor amit estas tronum."; SECTION("Basic 1") { - std::vector regex_patterns = { "tras", "do(\\w*)or", "es(\\w*)as", "ip(\\w*)um" }; - inja::SearchMatchVector match = inja::search(input, regex_patterns, 0); - CHECK( match.regex_number == 3 ); - CHECK( match.outer == "ipsum" ); - CHECK( match.inner == "s" ); + std::vector regex_patterns = { inja::Regex("tras"), inja::Regex("do(\\w*)or"), inja::Regex("es(\\w*)as"), inja::Regex("ip(\\w*)um") }; + inja::Match match = inja::search(input, regex_patterns, 0); + CHECK( match.regex_number() == 3 ); + CHECK( match.str() == "ipsum" ); + CHECK( match.str(1) == "s" ); } SECTION("Basic 2") { - std::vector regex_patterns = { "tras", "ip(\\w*)um", "do(\\w*)or", "es(\\w*)as" }; - inja::SearchMatchVector match = inja::search(input, regex_patterns, 0); - CHECK( match.regex_number == 1 ); - CHECK( match.outer == "ipsum" ); - CHECK( match.inner == "s" ); + std::vector regex_patterns = { inja::Regex("tras"), inja::Regex("ip(\\w*)um"), inja::Regex("do(\\w*)or"), inja::Regex("es(\\w*)as") }; + inja::Match match = inja::search(input, regex_patterns, 0); + CHECK( match.regex_number() == 1 ); + CHECK( match.str() == "ipsum" ); + CHECK( match.str(1) == "s" ); } } TEST_CASE("Search on level") { std::string input = "(% up %)(% up %)Test(% N1 %)(% down %)...(% up %)(% N2 %)(% up %)(% N3 %)(% down %)(% N4 %)(% down %)(% N5 %)(% down %)"; - std::regex regex_statement("\\(\\% (.*?) \\%\\)"); - std::regex regex_level_up("up"); - std::regex regex_level_down("down"); - std::regex regex_search("N(\\d+)"); + inja::Regex regex_statement("\\(\\% (.*?) \\%\\)"); + inja::Regex regex_level_up("up"); + inja::Regex regex_level_down("down"); + inja::Regex regex_search("N(\\d+)"); SECTION("First instance") { - inja::SearchMatch open_match = inja::search(input, regex_statement, 0); - CHECK( open_match.position == 0 ); - CHECK( open_match.end_position == 8 ); - CHECK( open_match.inner == "up" ); + inja::Match open_match = inja::search(input, regex_statement, 0); + CHECK( open_match.position() == 0 ); + CHECK( open_match.end_position() == 8 ); + CHECK( open_match.str(1) == "up" ); - inja::SearchClosedMatch match = inja::search_closed_match_on_level(input, regex_statement, regex_level_up, regex_level_down, regex_search, open_match); - CHECK( match.position == 0 ); - CHECK( match.end_position == 109 ); + inja::MatchClosed match = inja::search_closed_match_on_level(input, regex_statement, regex_level_up, regex_level_down, regex_search, open_match); + CHECK( match.position() == 0 ); + CHECK( match.end_position() == 109 ); } SECTION("Second instance") { - inja::SearchMatch open_match = inja::search(input, regex_statement, 4); + inja::Match open_match = inja::search(input, regex_statement, 4); - CHECK( open_match.position == 8 ); - CHECK( open_match.end_position == 16 ); - CHECK( open_match.inner == "up" ); + CHECK( open_match.position() == 8 ); + CHECK( open_match.end_position() == 16 ); + CHECK( open_match.str(1) == "up" ); - inja::SearchClosedMatch match = inja::search_closed_match_on_level(input, regex_statement, regex_level_up, regex_level_down, regex_search, open_match); + inja::MatchClosed match = inja::search_closed_match_on_level(input, regex_statement, regex_level_up, regex_level_down, regex_search, open_match); - CHECK( match.open_match.position == 8 ); - CHECK( match.open_match.end_position== 16 ); - CHECK( match.close_match.position == 20 ); - CHECK( match.close_match.end_position == 28 ); - CHECK( match.position == 8 ); - CHECK( match.end_position == 28 ); + CHECK( match.open_match.position() == 8 ); + CHECK( match.open_match.end_position() == 16 ); + CHECK( match.close_match.position() == 20 ); + 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" ); } diff --git a/test/src/unit.cpp b/test/src/unit.cpp index 063e878..0c7c351 100644 --- a/test/src/unit.cpp +++ b/test/src/unit.cpp @@ -1,2 +1,2 @@ #define CATCH_CONFIG_MAIN -#include "catch.hpp" \ No newline at end of file +#include "catch.hpp"