diff --git a/common/minja.hpp b/common/minja.hpp index 979e53fe0..9dc8ed243 100644 --- a/common/minja.hpp +++ b/common/minja.hpp @@ -20,12 +20,6 @@ using json = nlohmann::ordered_json; -/* Backport make_unique from C++14. */ -template -typename std::unique_ptr nonstd_make_unique(Args &&...args) { - return std::unique_ptr(new T(std::forward(args)...)); -} - namespace minja { class Context; @@ -36,42 +30,13 @@ struct Options { bool keep_trailing_newline; // don't remove last newline }; +struct ArgumentsValue; + /* Values that behave roughly like in Python. */ class Value : public std::enable_shared_from_this { public: - struct Arguments { - std::vector args; - std::vector> kwargs; - - bool has_named(const std::string & name) { - for (const auto & p : kwargs) { - if (p.first == name) return true; - } - return false; - } - - Value get_named(const std::string & name) { - for (const auto & p : kwargs) { - if (p.first == name) return p.second; - } - return Value(); - } - - bool empty() { - return args.empty() && kwargs.empty(); - } - - void expectArgs(const std::string & method_name, const std::pair & pos_count, const std::pair & kw_count) { - if (args.size() < pos_count.first || args.size() > pos_count.second || kwargs.size() < kw_count.first || kwargs.size() > kw_count.second) { - std::ostringstream out; - out << method_name << " must have between " << pos_count.first << " and " << pos_count.second << " positional arguments and between " << kw_count.first << " and " << kw_count.second << " keyword arguments"; - throw std::runtime_error(out.str()); - } - } - }; - - using CallableType = std::function &, Arguments &)>; - using FilterType = std::function &, Arguments &)>; + using CallableType = std::function &, ArgumentsValue &)>; + using FilterType = std::function &, ArgumentsValue &)>; private: using ObjectType = nlohmann::ordered_map; // Only contains primitive keys @@ -246,7 +211,7 @@ public: if (!key.is_hashable()) throw std::runtime_error("Unashable type: " + dump()); (*object_)[key.primitive_] = value; } - Value call(const std::shared_ptr & context, Value::Arguments & args) const { + Value call(const std::shared_ptr & context, ArgumentsValue & args) const { if (!callable_) throw std::runtime_error("Value is not callable: " + dump()); return (*callable_)(context, args); } @@ -305,6 +270,20 @@ public: return true; } + int64_t to_int() const { + if (is_null()) return 0; + if (is_boolean()) return get() ? 1 : 0; + if (is_number()) return static_cast(get()); + if (is_string()) { + try { + return std::stol(get()); + } catch (const std::exception &) { + return 0; + } + } + return 0; + } + bool operator<(const Value & other) const { if (is_null()) throw std::runtime_error("Undefined value or reference"); @@ -433,12 +412,18 @@ public: return dump(); } Value operator+(const Value& rhs) const { - if (is_string() || rhs.is_string()) + if (is_string() || rhs.is_string()) { return to_str() + rhs.to_str(); - else if (is_number_integer() && rhs.is_number_integer()) + } else if (is_number_integer() && rhs.is_number_integer()) { return get() + rhs.get(); - else + } else if (is_array() && rhs.is_array()) { + auto res = Value::array(); + for (const auto& item : *array_) res.push_back(item); + for (const auto& item : *rhs.array_) res.push_back(item); + return res; + } else { return get() + rhs.get(); + } } Value operator-(const Value& rhs) const { if (is_number_integer() && rhs.is_number_integer()) @@ -449,7 +434,7 @@ public: Value operator*(const Value& rhs) const { if (is_string() && rhs.is_number_integer()) { std::ostringstream out; - for (int i = 0, n = rhs.get(); i < n; ++i) { + for (int64_t i = 0, n = rhs.get(); i < n; ++i) { out << to_str(); } return out.str(); @@ -470,6 +455,37 @@ public: } }; +struct ArgumentsValue { + std::vector args; + std::vector> kwargs; + + bool has_named(const std::string & name) { + for (const auto & p : kwargs) { + if (p.first == name) return true; + } + return false; + } + + Value get_named(const std::string & name) { + for (const auto & [key, value] : kwargs) { + if (key == name) return value; + } + return Value(); + } + + bool empty() { + return args.empty() && kwargs.empty(); + } + + void expectArgs(const std::string & method_name, const std::pair & pos_count, const std::pair & kw_count) { + if (args.size() < pos_count.first || args.size() > pos_count.second || kwargs.size() < kw_count.first || kwargs.size() > kw_count.second) { + std::ostringstream out; + out << method_name << " must have between " << pos_count.first << " and " << pos_count.second << " positional arguments and between " << kw_count.first << " and " << kw_count.second << " keyword arguments"; + throw std::runtime_error(out.str()); + } + } +}; + template <> inline json Value::get() const { if (is_primitive()) return primitive_; @@ -483,13 +499,11 @@ inline json Value::get() const { } if (object_) { json res = json::object(); - for (const auto& item : *object_) { - const auto & key = item.first; - auto json_value = item.second.get(); + for (const auto& [key, value] : *object_) { if (key.is_string()) { - res[key.get()] = json_value; + res[key.get()] = value.get(); } else if (key.is_primitive()) { - res[key.dump()] = json_value; + res[key.dump()] = value.get(); } else { throw std::runtime_error("Invalid key type for conversion to JSON: " + key.dump()); } @@ -587,30 +601,6 @@ class Expression { protected: virtual Value do_evaluate(const std::shared_ptr & context) const = 0; public: - struct Arguments { - std::vector> args; - std::vector>> kwargs; - - void expectArgs(const std::string & method_name, const std::pair & pos_count, const std::pair & kw_count) const { - if (args.size() < pos_count.first || args.size() > pos_count.second || kwargs.size() < kw_count.first || kwargs.size() > kw_count.second) { - std::ostringstream out; - out << method_name << " must have between " << pos_count.first << " and " << pos_count.second << " positional arguments and between " << kw_count.first << " and " << kw_count.second << " keyword arguments"; - throw std::runtime_error(out.str()); - } - } - - Value::Arguments evaluate(const std::shared_ptr & context) const { - Value::Arguments vargs; - for (const auto& arg : this->args) { - vargs.args.push_back(arg->evaluate(context)); - } - for (const auto& arg : this->kwargs) { - vargs.kwargs.push_back({arg.first, arg.second->evaluate(context)}); - } - return vargs; - } - }; - using Parameters = std::vector>>; Location location; @@ -662,7 +652,7 @@ enum SpaceHandling { Keep, Strip, StripSpaces, StripNewline }; class TemplateToken { public: - enum class Type { Text, Expression, If, Else, Elif, EndIf, For, EndFor, Set, EndSet, Comment, Macro, EndMacro }; + enum class Type { Text, Expression, If, Else, Elif, EndIf, For, EndFor, Set, EndSet, Comment, Macro, EndMacro, Filter, EndFilter }; static std::string typeToString(Type t) { switch (t) { @@ -679,6 +669,8 @@ public: case Type::Comment: return "comment"; case Type::Macro: return "macro"; case Type::EndMacro: return "endmacro"; + case Type::Filter: return "filter"; + case Type::EndFilter: return "endfilter"; } return "Unknown"; } @@ -731,6 +723,16 @@ struct EndMacroTemplateToken : public TemplateToken { EndMacroTemplateToken(const Location & location, SpaceHandling pre, SpaceHandling post) : TemplateToken(Type::EndMacro, location, pre, post) {} }; +struct FilterTemplateToken : public TemplateToken { + std::shared_ptr filter; + FilterTemplateToken(const Location & location, SpaceHandling pre, SpaceHandling post, std::shared_ptr && filter) + : TemplateToken(Type::Filter, location, pre, post), filter(std::move(filter)) {} +}; + +struct EndFilterTemplateToken : public TemplateToken { + EndFilterTemplateToken(const Location & location, SpaceHandling pre, SpaceHandling post) : TemplateToken(Type::EndFilter, location, pre, post) {} +}; + struct ForTemplateToken : public TemplateToken { std::vector var_names; std::shared_ptr iterable; @@ -886,7 +888,7 @@ public: loop.set("length", (int64_t) filtered_items.size()); size_t cycle_index = 0; - loop.set("cycle", Value::callable([&](const std::shared_ptr &, Value::Arguments & args) { + loop.set("cycle", Value::callable([&](const std::shared_ptr &, ArgumentsValue & args) { if (args.args.empty() || !args.kwargs.empty()) { throw std::runtime_error("cycle() expects at least 1 positional argument and no named arg"); } @@ -914,7 +916,7 @@ public: }; if (recursive) { - loop_function = [&](const std::shared_ptr &, Value::Arguments & args) { + loop_function = [&](const std::shared_ptr &, ArgumentsValue & args) { if (args.args.size() != 1 || !args.kwargs.empty() || !args.args[0].is_array()) { throw std::runtime_error("loop() expects exactly 1 positional iterable argument"); } @@ -946,7 +948,7 @@ public: void do_render(std::ostringstream &, const std::shared_ptr & macro_context) const override { if (!name) throw std::runtime_error("MacroNode.name is null"); if (!body) throw std::runtime_error("MacroNode.body is null"); - auto callable = Value::callable([&](const std::shared_ptr & context, Value::Arguments & args) { + auto callable = Value::callable([&](const std::shared_ptr & context, ArgumentsValue & args) { auto call_context = macro_context; std::vector param_set(params.size(), false); for (size_t i = 0, n = args.args.size(); i < n; i++) { @@ -956,13 +958,11 @@ public: auto & param_name = params[i].first; call_context->set(param_name, arg); } - for (size_t i = 0, n = args.kwargs.size(); i < n; i++) { - auto & arg = args.kwargs[i]; - auto & arg_name = arg.first; + for (auto & [arg_name, value] : args.kwargs) { auto it = named_param_positions.find(arg_name); if (it == named_param_positions.end()) throw std::runtime_error("Unknown parameter name for macro " + name->get_name() + ": " + arg_name); - call_context->set(arg_name, arg.second); + call_context->set(arg_name, value); param_set[it->second] = true; } // Set default values for parameters that were not passed @@ -978,6 +978,29 @@ public: } }; +class FilterNode : public TemplateNode { + std::shared_ptr filter; + std::shared_ptr body; + +public: + FilterNode(const Location & location, std::shared_ptr && f, std::shared_ptr && b) + : TemplateNode(location), filter(std::move(f)), body(std::move(b)) {} + + void do_render(std::ostringstream & out, const std::shared_ptr & context) const override { + if (!filter) throw std::runtime_error("FilterNode.filter is null"); + if (!body) throw std::runtime_error("FilterNode.body is null"); + auto filter_value = filter->evaluate(context); + if (!filter_value.is_callable()) { + throw std::runtime_error("Filter must be a callable: " + filter_value.dump()); + } + std::string rendered_body = body->render(context); + + ArgumentsValue filter_args = {{Value(rendered_body)}, {}}; + auto result = filter_value.call(context, filter_args); + out << result.to_str(); + } +}; + class SetNode : public TemplateNode { std::string ns; std::vector var_names; @@ -1065,10 +1088,10 @@ public: : Expression(location), elements(std::move(e)) {} Value do_evaluate(const std::shared_ptr & context) const override { auto result = Value::object(); - for (const auto& e : elements) { - if (!e.first) throw std::runtime_error("Dict key is null"); - if (!e.second) throw std::runtime_error("Dict value is null"); - result.set(e.first->evaluate(context), e.second->evaluate(context)); + for (const auto& [key, value] : elements) { + if (!key) throw std::runtime_error("Dict key is null"); + if (!value) throw std::runtime_error("Dict value is null"); + result.set(key->evaluate(context), value->evaluate(context)); } return result; } @@ -1128,11 +1151,9 @@ public: class UnaryOpExpr : public Expression { public: - enum class Op { Plus, Minus, LogicalNot }; -private: + enum class Op { Plus, Minus, LogicalNot, Expansion, ExpansionDict }; std::shared_ptr expr; Op op; -public: UnaryOpExpr(const Location & location, std::shared_ptr && e, Op o) : Expression(location), expr(std::move(e)), op(o) {} Value do_evaluate(const std::shared_ptr & context) const override { @@ -1142,6 +1163,10 @@ public: case Op::Plus: return e; case Op::Minus: return -e; case Op::LogicalNot: return !e.to_bool(); + case Op::Expansion: + case Op::ExpansionDict: + throw std::runtime_error("Expansion operator is only supported in function calls and collections"); + } throw std::runtime_error("Unknown unary operator"); } @@ -1217,7 +1242,7 @@ public: }; if (l.is_callable()) { - return Value::callable([l, do_eval](const std::shared_ptr & context, Value::Arguments & args) { + return Value::callable([l, do_eval](const std::shared_ptr & context, ArgumentsValue & args) { auto ll = l.call(context, args); return do_eval(ll); //args[0].second); }); @@ -1227,6 +1252,43 @@ public: } }; +struct ArgumentsExpression { + std::vector> args; + std::vector>> kwargs; + + ArgumentsValue evaluate(const std::shared_ptr & context) const { + ArgumentsValue vargs; + for (const auto& arg : this->args) { + if (auto un_expr = std::dynamic_pointer_cast(arg)) { + if (un_expr->op == UnaryOpExpr::Op::Expansion) { + auto array = un_expr->expr->evaluate(context); + if (!array.is_array()) { + throw std::runtime_error("Expansion operator only supported on arrays"); + } + array.for_each([&](Value & value) { + vargs.args.push_back(value); + }); + continue; + } else if (un_expr->op == UnaryOpExpr::Op::ExpansionDict) { + auto dict = un_expr->expr->evaluate(context); + if (!dict.is_object()) { + throw std::runtime_error("ExpansionDict operator only supported on objects"); + } + dict.for_each([&](const Value & key) { + vargs.kwargs.push_back({key.get(), dict.at(key)}); + }); + continue; + } + } + vargs.args.push_back(arg->evaluate(context)); + } + for (const auto& [name, value] : this->kwargs) { + vargs.kwargs.push_back({name, value->evaluate(context)}); + } + return vargs; + } +}; + static std::string strip(const std::string & s) { static std::regex trailing_spaces_regex("^\\s+|\\s+$"); return std::regex_replace(s, trailing_spaces_regex, ""); @@ -1251,64 +1313,64 @@ static std::string html_escape(const std::string & s) { class MethodCallExpr : public Expression { std::shared_ptr object; std::shared_ptr method; - Expression::Arguments args; + ArgumentsExpression args; public: - MethodCallExpr(const Location & location, std::shared_ptr && obj, std::shared_ptr && m, Expression::Arguments && a) + MethodCallExpr(const Location & location, std::shared_ptr && obj, std::shared_ptr && m, ArgumentsExpression && a) : Expression(location), object(std::move(obj)), method(std::move(m)), args(std::move(a)) {} Value do_evaluate(const std::shared_ptr & context) const override { if (!object) throw std::runtime_error("MethodCallExpr.object is null"); if (!method) throw std::runtime_error("MethodCallExpr.method is null"); auto obj = object->evaluate(context); + auto vargs = args.evaluate(context); if (obj.is_null()) { throw std::runtime_error("Trying to call method '" + method->get_name() + "' on null"); } if (obj.is_array()) { if (method->get_name() == "append") { - args.expectArgs("append method", {1, 1}, {0, 0}); - obj.push_back(args.args[0]->evaluate(context)); + vargs.expectArgs("append method", {1, 1}, {0, 0}); + obj.push_back(vargs.args[0]); return Value(); } else if (method->get_name() == "insert") { - args.expectArgs("insert method", {2, 2}, {0, 0}); - auto index = args.args[0]->evaluate(context).get(); + vargs.expectArgs("insert method", {2, 2}, {0, 0}); + auto index = vargs.args[0].get(); if (index < 0 || index > (int64_t) obj.size()) throw std::runtime_error("Index out of range for insert method"); - obj.insert(index, args.args[1]->evaluate(context)); + obj.insert(index, vargs.args[1]); return Value(); } } else if (obj.is_object()) { if (method->get_name() == "items") { - args.expectArgs("items method", {0, 0}, {0, 0}); + vargs.expectArgs("items method", {0, 0}, {0, 0}); auto result = Value::array(); for (const auto& key : obj.keys()) { result.push_back(Value::array({key, obj.at(key)})); } return result; } else if (method->get_name() == "get") { - args.expectArgs("get method", {1, 2}, {0, 0}); - auto key = args.args[0]->evaluate(context); - if (args.args.size() == 1) { + vargs.expectArgs("get method", {1, 2}, {0, 0}); + auto key = vargs.args[0]; + if (vargs.args.size() == 1) { return obj.contains(key) ? obj.at(key) : Value(); } else { - return obj.contains(key) ? obj.at(key) : args.args[1]->evaluate(context); + return obj.contains(key) ? obj.at(key) : vargs.args[1]; } } else if (obj.contains(method->get_name())) { auto callable = obj.at(method->get_name()); if (!callable.is_callable()) { throw std::runtime_error("Property '" + method->get_name() + "' is not callable"); } - Value::Arguments vargs = args.evaluate(context); return callable.call(context, vargs); } } else if (obj.is_string()) { auto str = obj.get(); if (method->get_name() == "strip") { - args.expectArgs("strip method", {0, 0}, {0, 0}); + vargs.expectArgs("strip method", {0, 0}, {0, 0}); return Value(strip(str)); } else if (method->get_name() == "endswith") { - args.expectArgs("endswith method", {1, 1}, {0, 0}); - auto suffix = args.args[0]->evaluate(context).get(); + vargs.expectArgs("endswith method", {1, 1}, {0, 0}); + auto suffix = vargs.args[0].get(); return suffix.length() <= str.length() && std::equal(suffix.rbegin(), suffix.rend(), str.rbegin()); } else if (method->get_name() == "title") { - args.expectArgs("title method", {0, 0}, {0, 0}); + vargs.expectArgs("title method", {0, 0}, {0, 0}); auto res = str; for (size_t i = 0, n = res.size(); i < n; ++i) { if (i == 0 || std::isspace(res[i - 1])) res[i] = std::toupper(res[i]); @@ -1324,8 +1386,8 @@ public: class CallExpr : public Expression { public: std::shared_ptr object; - Expression::Arguments args; - CallExpr(const Location & location, std::shared_ptr && obj, Expression::Arguments && a) + ArgumentsExpression args; + CallExpr(const Location & location, std::shared_ptr && obj, ArgumentsExpression && a) : Expression(location), object(std::move(obj)), args(std::move(a)) {} Value do_evaluate(const std::shared_ptr & context) const override { if (!object) throw std::runtime_error("CallExpr.object is null"); @@ -1354,12 +1416,12 @@ public: } else { if (auto ce = dynamic_cast(part.get())) { auto target = ce->object->evaluate(context); - Value::Arguments args = ce->args.evaluate(context); + ArgumentsValue args = ce->args.evaluate(context); args.args.insert(args.args.begin(), result); result = target.call(context, args); } else { auto callable = part->evaluate(context); - Value::Arguments args; + ArgumentsValue args; args.args.insert(args.args.begin(), result); result = callable.call(context, args); } @@ -1421,7 +1483,7 @@ private: escape = true; } else if (*it == quote) { ++it; - return nonstd_make_unique(std::move(result)); + return std::make_unique(std::move(result)); } else { result += *it; } @@ -1568,8 +1630,8 @@ private: } auto location = get_location(); - auto if_expr = parseIfExpression(); - return std::make_shared(location, std::move(if_expr.first), std::move(left), std::move(if_expr.second)); + auto [condition, else_expr] = parseIfExpression(); + return std::make_shared(location, std::move(condition), std::move(left), std::move(else_expr)); } Location get_location() const { @@ -1586,7 +1648,7 @@ private: else_expr = parseExpression(); if (!else_expr) throw std::runtime_error("Expected 'else' expression"); } - return std::make_pair(std::move(condition), std::move(else_expr)); + return std::pair(std::move(condition), std::move(else_expr)); } std::shared_ptr parseLogicalOr() { @@ -1700,11 +1762,11 @@ private: throw std::runtime_error("Expected closing parenthesis in call args"); } - Expression::Arguments parseCallArgs() { + ArgumentsExpression parseCallArgs() { consumeSpaces(); if (consumeToken("(").empty()) throw std::runtime_error("Expected opening parenthesis in call args"); - Expression::Arguments result; + ArgumentsExpression result; while (it != end) { if (!consumeToken(")").empty()) { @@ -1815,15 +1877,15 @@ private: return left; } - std::shared_ptr call_func(const std::string & name, Expression::Arguments && args) const { + std::shared_ptr call_func(const std::string & name, ArgumentsExpression && args) const { return std::make_shared(get_location(), std::make_shared(get_location(), name), std::move(args)); } std::shared_ptr parseMathUnaryPlusMinus() { static std::regex unary_plus_minus_tok(R"(\+|-(?![}%#]\}))"); auto op_str = consumeToken(unary_plus_minus_tok); - auto expr = parseValueExpression(); - if (!expr) throw std::runtime_error("Expected expr of 'unary plus/minus' expression"); + auto expr = parseExpansion(); + if (!expr) throw std::runtime_error("Expected expr of 'unary plus/minus/expansion' expression"); if (!op_str.empty()) { auto op = op_str == "+" ? UnaryOpExpr::Op::Plus : UnaryOpExpr::Op::Minus; @@ -1832,6 +1894,15 @@ private: return expr; } + std::shared_ptr parseExpansion() { + static std::regex expansion_tok(R"(\*\*?)"); + auto op_str = consumeToken(expansion_tok); + auto expr = parseValueExpression(); + if (op_str.empty()) return expr; + if (!expr) throw std::runtime_error("Expected expr of 'expansion' expression"); + return std::make_shared(get_location(), std::move(expr), op_str == "*" ? UnaryOpExpr::Op::Expansion : UnaryOpExpr::Op::ExpansionDict); + } + std::shared_ptr parseValueExpression() { auto parseValue = [&]() -> std::shared_ptr { auto location = get_location(); @@ -1971,7 +2042,7 @@ private: if (consumeToken(":").empty()) throw std::runtime_error("Expected colon betweek key & value in dictionary"); auto value = parseExpression(); if (!value) throw std::runtime_error("Expected value in dictionary"); - elements.emplace_back(std::make_pair(std::move(key), std::move(value))); + elements.emplace_back(std::pair(std::move(key), std::move(value))); }; parseKeyValuePair(); @@ -2029,7 +2100,7 @@ private: static std::regex comment_tok(R"(\{#([-~]?)(.*?)([-~]?)#\})"); static std::regex expr_open_regex(R"(\{\{([-~])?)"); static std::regex block_open_regex(R"(^\{%([-~])?[\s\n\r]*)"); - static std::regex block_keyword_tok(R"((if|else|elif|endif|for|endfor|set|endset|block|endblock|macro|endmacro)\b)"); + static std::regex block_keyword_tok(R"((if|else|elif|endif|for|endfor|set|endset|block|endblock|macro|endmacro|filter|endfilter)\b)"); static std::regex text_regex(R"([\s\S\n\r]*?($|(?=\{\{|\{%|\{#)))"); static std::regex expr_close_regex(R"([\s\n\r]*([-~])?\}\})"); static std::regex block_close_regex(R"([\s\n\r]*([-~])?%\})"); @@ -2046,7 +2117,7 @@ private: auto pre_space = parsePreSpace(group[1]); auto content = group[2]; auto post_space = parsePostSpace(group[3]); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space, content)); + tokens.push_back(std::make_unique(location, pre_space, post_space, content)); } else if (!(group = consumeTokenGroups(expr_open_regex, SpaceHandling::Keep)).empty()) { auto pre_space = parsePreSpace(group[1]); auto expr = parseExpression(); @@ -2056,7 +2127,7 @@ private: } auto post_space = parsePostSpace(group[1]); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space, std::move(expr))); + tokens.push_back(std::make_unique(location, pre_space, post_space, std::move(expr))); } else if (!(group = consumeTokenGroups(block_open_regex, SpaceHandling::Keep)).empty()) { auto pre_space = parsePreSpace(group[1]); @@ -2074,19 +2145,19 @@ private: if (!condition) throw std::runtime_error("Expected condition in if block"); auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space, std::move(condition))); + tokens.push_back(std::make_unique(location, pre_space, post_space, std::move(condition))); } else if (keyword == "elif") { auto condition = parseExpression(); if (!condition) throw std::runtime_error("Expected condition in elif block"); auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space, std::move(condition))); + tokens.push_back(std::make_unique(location, pre_space, post_space, std::move(condition))); } else if (keyword == "else") { auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space)); + tokens.push_back(std::make_unique(location, pre_space, post_space)); } else if (keyword == "endif") { auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space)); + tokens.push_back(std::make_unique(location, pre_space, post_space)); } else if (keyword == "for") { static std::regex recursive_tok(R"(recursive\b)"); static std::regex if_tok(R"(if\b)"); @@ -2104,10 +2175,10 @@ private: auto recursive = !consumeToken(recursive_tok).empty(); auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space, std::move(varnames), std::move(iterable), std::move(condition), recursive)); + tokens.push_back(std::make_unique(location, pre_space, post_space, std::move(varnames), std::move(iterable), std::move(condition), recursive)); } else if (keyword == "endfor") { auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space)); + tokens.push_back(std::make_unique(location, pre_space, post_space)); } else if (keyword == "set") { static std::regex namespaced_var_regex(R"((\w+)[\s\n\r]*\.[\s\n\r]*(\w+))"); @@ -2131,25 +2202,34 @@ private: } } auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space, ns, var_names, std::move(value))); + tokens.push_back(std::make_unique(location, pre_space, post_space, ns, var_names, std::move(value))); } else if (keyword == "endset") { auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space)); + tokens.push_back(std::make_unique(location, pre_space, post_space)); } else if (keyword == "macro") { auto macroname = parseIdentifier(); if (!macroname) throw std::runtime_error("Expected macro name in macro block"); auto params = parseParameters(); auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space, std::move(macroname), std::move(params))); + tokens.push_back(std::make_unique(location, pre_space, post_space, std::move(macroname), std::move(params))); } else if (keyword == "endmacro") { auto post_space = parseBlockClose(); - tokens.push_back(nonstd_make_unique(location, pre_space, post_space)); + tokens.push_back(std::make_unique(location, pre_space, post_space)); + } else if (keyword == "filter") { + auto filter = parseExpression(); + if (!filter) throw std::runtime_error("Expected expression in filter block"); + + auto post_space = parseBlockClose(); + tokens.push_back(std::make_unique(location, pre_space, post_space, std::move(filter))); + } else if (keyword == "endfilter") { + auto post_space = parseBlockClose(); + tokens.push_back(std::make_unique(location, pre_space, post_space)); } else { throw std::runtime_error("Unexpected block: " + keyword); } } else if (!(text = consumeToken(text_regex, SpaceHandling::Keep)).empty()) { - tokens.push_back(nonstd_make_unique(location, SpaceHandling::Keep, SpaceHandling::Keep, text)); + tokens.push_back(std::make_unique(location, SpaceHandling::Keep, SpaceHandling::Keep, text)); } else { if (it != end) throw std::runtime_error("Unexpected character"); } @@ -2241,11 +2321,18 @@ private: throw unterminated(**start); } children.emplace_back(std::make_shared(token->location, std::move(macro_token->name), std::move(macro_token->params), std::move(body))); + } else if (auto filter_token = dynamic_cast(token.get())) { + auto body = parseTemplate(begin, it, end); + if (it == end || (*(it++))->type != TemplateToken::Type::EndFilter) { + throw unterminated(**start); + } + children.emplace_back(std::make_shared(token->location, std::move(filter_token->filter), std::move(body))); } else if (dynamic_cast(token.get())) { // Ignore comments } else if (dynamic_cast(token.get()) || dynamic_cast(token.get()) || dynamic_cast(token.get()) + || dynamic_cast(token.get()) || dynamic_cast(token.get()) || dynamic_cast(token.get()) || dynamic_cast(token.get())) { @@ -2283,7 +2370,7 @@ static Value simple_function(const std::string & fn_name, const std::vector named_positions; for (size_t i = 0, n = params.size(); i < n; i++) named_positions[params[i]] = i; - return Value::callable([=](const std::shared_ptr & context, Value::Arguments & args) -> Value { + return Value::callable([=](const std::shared_ptr & context, ArgumentsValue & args) -> Value { auto args_obj = Value::object(); std::vector provided_args(params.size()); for (size_t i = 0, n = args.args.size(); i < n; i++) { @@ -2295,14 +2382,13 @@ static Value simple_function(const std::string & fn_name, const std::vectorsecond] = true; - args_obj.set(arg.first, arg.second); + args_obj.set(name, value); } return fn(context, args_obj); }); @@ -2344,6 +2430,29 @@ inline std::shared_ptr Context::builtins() { auto & text = args.at("text"); return text.is_null() ? text : Value(strip(text.get())); })); + globals.set("lower", simple_function("lower", { "text" }, [](const std::shared_ptr &, Value & args) { + auto text = args.at("text"); + if (text.is_null()) return text; + std::string res; + auto str = text.get(); + std::transform(str.begin(), str.end(), std::back_inserter(res), ::tolower); + return Value(res); + })); + globals.set("default", Value::callable([=](const std::shared_ptr &, ArgumentsValue & args) { + args.expectArgs("default", {2, 3}, {0, 1}); + auto & value = args.args[0]; + auto & default_value = args.args[1]; + bool boolean = false; + if (args.args.size() == 3) { + boolean = args.args[2].get(); + } else { + Value bv = args.get_named("boolean"); + if (!bv.is_null()) { + boolean = bv.get(); + } + } + return boolean ? (value.to_bool() ? value : default_value) : value.is_null() ? default_value : value; + })); auto escape = simple_function("escape", { "text" }, [](const std::shared_ptr &, Value & args) { return Value(html_escape(args.at("text").get())); }); @@ -2398,11 +2507,11 @@ inline std::shared_ptr Context::builtins() { }); } })); - globals.set("namespace", Value::callable([=](const std::shared_ptr &, Value::Arguments & args) { + globals.set("namespace", Value::callable([=](const std::shared_ptr &, ArgumentsValue & args) { auto ns = Value::object(); args.expectArgs("namespace", {0, 0}, {0, std::numeric_limits::max()}); - for (auto & arg : args.kwargs) { - ns.set(arg.first, arg.second); + for (auto & [name, value] : args.kwargs) { + ns.set(name, value); } return ns; })); @@ -2419,8 +2528,10 @@ inline std::shared_ptr Context::builtins() { return args.at("value"); })); globals.set("string", simple_function("string", { "value" }, [](const std::shared_ptr &, Value & args) -> Value { - auto & items = args.at("value"); - return items.to_str(); + return args.at("value").to_str(); + })); + globals.set("int", simple_function("int", { "value" }, [](const std::shared_ptr &, Value & args) -> Value { + return args.at("value").to_int(); })); globals.set("list", simple_function("list", { "items" }, [](const std::shared_ptr &, Value & args) -> Value { auto & items = args.at("items"); @@ -2443,7 +2554,7 @@ inline std::shared_ptr Context::builtins() { auto make_filter = [](const Value & filter, Value & extra_args) -> Value { return simple_function("", { "value" }, [=](const std::shared_ptr & context, Value & args) { auto & value = args.at("value"); - Value::Arguments actual_args; + ArgumentsValue actual_args; actual_args.args.emplace_back(value); for (size_t i = 0, n = extra_args.size(); i < n; i++) { actual_args.args.emplace_back(extra_args.at(i)); @@ -2452,7 +2563,7 @@ inline std::shared_ptr Context::builtins() { }); }; // https://jinja.palletsprojects.com/en/3.0.x/templates/#jinja-filters.reject - globals.set("reject", Value::callable([=](const std::shared_ptr & context, Value::Arguments & args) { + globals.set("reject", Value::callable([=](const std::shared_ptr & context, ArgumentsValue & args) { args.expectArgs("reject", {2, std::numeric_limits::max()}, {0, 0}); auto & items = args.args[0]; auto filter_fn = context->get(args.args[1]); @@ -2467,7 +2578,7 @@ inline std::shared_ptr Context::builtins() { auto res = Value::array(); for (size_t i = 0, n = items.size(); i < n; i++) { auto & item = items.at(i); - Value::Arguments filter_args; + ArgumentsValue filter_args; filter_args.args.emplace_back(item); auto pred_res = filter.call(context, filter_args); if (!pred_res.to_bool()) { @@ -2476,7 +2587,7 @@ inline std::shared_ptr Context::builtins() { } return res; })); - globals.set("map", Value::callable([=](const std::shared_ptr & context, Value::Arguments & args) { + globals.set("map", Value::callable([=](const std::shared_ptr & context, ArgumentsValue & args) { auto res = Value::array(); if (args.args.size() == 1 && ((args.has_named("attribute") && args.kwargs.size() == 1) || (args.has_named("default") && args.kwargs.size() == 2))) { @@ -2491,7 +2602,7 @@ inline std::shared_ptr Context::builtins() { } else if (args.kwargs.empty() && args.args.size() >= 2) { auto fn = context->get(args.args[1]); if (fn.is_null()) throw std::runtime_error("Undefined filter: " + args.args[1].dump()); - Value::Arguments filter_args { {Value()}, {} }; + ArgumentsValue filter_args { {Value()}, {} }; for (size_t i = 2, n = args.args.size(); i < n; i++) { filter_args.args.emplace_back(args.args[i]); } @@ -2523,7 +2634,7 @@ inline std::shared_ptr Context::builtins() { if (!text.empty() && text.back() == '\n') out += "\n"; return out; })); - globals.set("selectattr", Value::callable([=](const std::shared_ptr & context, Value::Arguments & args) { + globals.set("selectattr", Value::callable([=](const std::shared_ptr & context, ArgumentsValue & args) { args.expectArgs("selectattr", {2, std::numeric_limits::max()}, {0, 0}); auto & items = args.args[0]; if (items.is_null()) @@ -2532,7 +2643,7 @@ inline std::shared_ptr Context::builtins() { bool has_test = false; Value test_fn; - Value::Arguments test_args {{Value()}, {}}; + ArgumentsValue test_args {{Value()}, {}}; if (args.args.size() >= 3) { has_test = true; test_fn = context->get(args.args[2]); @@ -2558,7 +2669,7 @@ inline std::shared_ptr Context::builtins() { } return res; })); - globals.set("range", Value::callable([=](const std::shared_ptr &, Value::Arguments & args) { + globals.set("range", Value::callable([=](const std::shared_ptr &, ArgumentsValue & args) { std::vector startEndStep(3); std::vector param_set(3); if (args.args.size() == 1) { @@ -2572,17 +2683,17 @@ inline std::shared_ptr Context::builtins() { param_set[i] = true; } } - for (auto & arg : args.kwargs) { + for (auto & [name, value] : args.kwargs) { size_t i; - if (arg.first == "start") i = 0; - else if (arg.first == "end") i = 1; - else if (arg.first == "step") i = 2; - else throw std::runtime_error("Unknown argument " + arg.first + " for function range"); + if (name == "start") i = 0; + else if (name == "end") i = 1; + else if (name == "step") i = 2; + else throw std::runtime_error("Unknown argument " + name + " for function range"); if (param_set[i]) { - throw std::runtime_error("Duplicate argument " + arg.first + " for function range"); + throw std::runtime_error("Duplicate argument " + name + " for function range"); } - startEndStep[i] = arg.second.get(); + startEndStep[i] = value.get(); param_set[i] = true; } if (!param_set[1]) {