diff --git a/README.md b/README.md index 03fe7ec..37eb362 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ render(R"(Guest List: 2: Pierre 3: Tom */ ``` -In a loop, the special variables `index (number)`, `index1 (number)`, `is_first (boolean)` and `is_last (boolean)` are available. +In a loop, the special variables `index (number)`, `index1 (number)`, `is_first (boolean)` and `is_last (boolean)` are available. You can also iterate over objects like `{% for key, value in time %}`. #### Conditions diff --git a/src/inja.hpp b/src/inja.hpp index 6073169..df6cc8e 100644 --- a/src/inja.hpp +++ b/src/inja.hpp @@ -260,7 +260,8 @@ struct Parsed { }; enum class Loop { - ForIn + ForListIn, + ForMapIn }; struct Element { @@ -295,10 +296,13 @@ struct Parsed { typedef std::vector Arguments; struct ElementLoop: public Element { - const std::string item; + Loop loop; + const std::string key; + const std::string value; const ElementExpression list; - explicit ElementLoop(const std::string& item, const ElementExpression& list, const std::string& inner): Element(Type::Loop, inner), item(item), list(list) { } + explicit ElementLoop(const Loop loop_, const std::string& value, const ElementExpression& list, const std::string& inner): Element(Type::Loop, inner), loop(loop_), value(value), list(list) { } + explicit ElementLoop(const Loop loop_, const std::string& key, const std::string& value, const ElementExpression& list, const std::string& inner): Element(Type::Loop, inner), loop(loop_), key(key), value(value), list(list) { } }; struct ElementConditionContainer: public Element { @@ -402,9 +406,9 @@ public: return (eval_expression(element.args[0], data) or eval_expression(element.args[1], data)); } case Parsed::Function::In: { - const json item = eval_expression(element.args[0], data); + const json value = eval_expression(element.args[0], data); const json list = eval_expression(element.args[1], data); - return (std::find(list.begin(), list.end(), item) != list.end()); + return (std::find(list.begin(), list.end(), value) != list.end()); } case Parsed::Function::Equal: { return eval_expression(element.args[0], data) == eval_expression(element.args[1], data); @@ -484,18 +488,32 @@ public: } case Parsed::Type::Loop: { auto element_loop = std::static_pointer_cast(element); - const std::string item_name = element_loop->item; - - const std::vector list = eval_expression>(element_loop->list, data); - for (unsigned int i = 0; i < list.size(); i++) { - json data_loop = data; - data_loop[item_name] = list[i]; - data_loop["index"] = i; - data_loop["index1"] = i + 1; - data_loop["is_first"] = (i == 0); - data_loop["is_last"] = (i == list.size() - 1); - result += render(Template(*element_loop), data_loop); + switch (element_loop->loop) { + case Parsed::Loop::ForListIn: { + const std::vector list = eval_expression>(element_loop->list, data); + for (unsigned int i = 0; i < list.size(); i++) { + json data_loop = data; + data_loop[element_loop->value] = list[i]; + data_loop["index"] = i; + data_loop["index1"] = i + 1; + data_loop["is_first"] = (i == 0); + data_loop["is_last"] = (i == list.size() - 1); + result += render(Template(*element_loop), data_loop); + } + break; + } + case Parsed::Loop::ForMapIn: { + const std::map map = eval_expression>(element_loop->list, data); + for (auto const& item : map) { + json data_loop = data; + data_loop[element_loop->key] = item.first; + data_loop[element_loop->value] = item.second; + result += render(Template(*element_loop), data_loop); + } + break; + } } + break; } case Parsed::Type::Condition: { @@ -552,7 +570,8 @@ public: }; const std::map regex_map_loop = { - {Parsed::Loop::ForIn, Regex{"for (\\w+) in (.+)"}}, + {Parsed::Loop::ForListIn, Regex{"for (\\w+) in (.+)"}}, + {Parsed::Loop::ForMapIn, Regex{"for (\\w+), (\\w+) in (.+)"}}, }; const std::map regex_map_condition = { @@ -651,10 +670,26 @@ public: const std::string loop_inner = match_statement.str(0); MatchType match_command = match(loop_inner, regex_map_loop); - const std::string item_name = match_command.str(1); - const std::string list_name = match_command.str(2); + switch (match_command.type()) { + case Parsed::Loop::ForListIn: { + const std::string value_name = match_command.str(1); + const std::string list_name = match_command.str(2); - result.emplace_back( std::make_shared(item_name, parse_expression(list_name), loop_match.inner())); + result.emplace_back( std::make_shared(match_command.type(), value_name, parse_expression(list_name), loop_match.inner())); + break; + } + case Parsed::Loop::ForMapIn: { + const std::string key_name = match_command.str(1); + const std::string value_name = match_command.str(2); + const std::string list_name = match_command.str(3); + + result.emplace_back( std::make_shared(match_command.type(), key_name, value_name, parse_expression(list_name), loop_match.inner())); + break; + } + default: { + throw std::runtime_error("Unknown loop statement."); + } + } break; } case Parsed::Statement::Condition: { diff --git a/test/src/unit-renderer.cpp b/test/src/unit-renderer.cpp index 7cf9a14..1e9f651 100644 --- a/test/src/unit-renderer.cpp +++ b/test/src/unit-renderer.cpp @@ -19,6 +19,10 @@ TEST_CASE("types") { data["brother"]["daughter0"] = { { "name", "Maria" } }; data["is_happy"] = true; data["is_sad"] = false; + data["relatives"]["mother"] = "Maria"; + data["relatives"]["brother"] = "Chris"; + data["relatives"]["sister"] = "Jenny"; + SECTION("basic") { CHECK( env.render("", data) == "" ); @@ -47,6 +51,10 @@ TEST_CASE("types") { CHECK( env.render("{% for name in names %}a{% endfor %}", data) == "aa" ); 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("{% for type, name in relatives %}{{ type }}: {{ name }}, {% endfor %}", data) == "brother: Chris, mother: Maria, sister: Jenny, " ); + + CHECK_THROWS_WITH( env.render("{% for name ins names %}a{% endfor %}", data), "Unknown loop statement." ); + // CHECK_THROWS_WITH( env.render("{% for name in relatives %}{{ name }}{% endfor %}", data), "[json.exception.type_error.302] type must be array, but is object" ); } SECTION("conditionals") {