Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Minja supports the following subset of the [Jinja2/3 template syntax](https://ji
- `for` (`recursive`) (`if`) / `else` / `endfor` w/ `loop.*` (including `loop.cycle`) and destructuring
- `set` w/ namespaces & destructuring
- `macro` / `endmacro`
- `filter` / `endfilter`
- Extensible filters collection: `count`, `dictsort`, `equalto`, `e` / `escape`, `items`, `join`, `joiner`, `namespace`, `raise_exception`, `range`, `reject`, `tojson`, `trim`

Main limitations (non-exhaustive list):
Expand Down
55 changes: 53 additions & 2 deletions include/minja/minja.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,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) {
Expand All @@ -679,6 +679,8 @@ class TemplateToken {
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";
}
Expand Down Expand Up @@ -731,6 +733,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<Expression> filter;
FilterTemplateToken(const Location & location, SpaceHandling pre, SpaceHandling post, std::shared_ptr<Expression> && 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<std::string> var_names;
std::shared_ptr<Expression> iterable;
Expand Down Expand Up @@ -978,6 +990,29 @@ class MacroNode : public TemplateNode {
}
};

class FilterNode : public TemplateNode {
std::shared_ptr<Expression> filter;
std::shared_ptr<TemplateNode> body;

public:
FilterNode(const Location & location, std::shared_ptr<Expression> && f, std::shared_ptr<TemplateNode> && b)
: TemplateNode(location), filter(std::move(f)), body(std::move(b)) {}

void do_render(std::ostringstream & out, const std::shared_ptr<Context> & 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);

Value::Arguments 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<std::string> var_names;
Expand Down Expand Up @@ -2029,7 +2064,7 @@ class Parser {
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]*([-~])?%\})");
Expand Down Expand Up @@ -2145,6 +2180,15 @@ class Parser {
} else if (keyword == "endmacro") {
auto post_space = parseBlockClose();
tokens.push_back(nonstd_make_unique<EndMacroTemplateToken>(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(nonstd_make_unique<FilterTemplateToken>(location, pre_space, post_space, std::move(filter)));
} else if (keyword == "endfilter") {
auto post_space = parseBlockClose();
tokens.push_back(nonstd_make_unique<EndFilterTemplateToken>(location, pre_space, post_space));
} else {
throw std::runtime_error("Unexpected block: " + keyword);
}
Expand Down Expand Up @@ -2241,11 +2285,18 @@ class Parser {
throw unterminated(**start);
}
children.emplace_back(std::make_shared<MacroNode>(token->location, std::move(macro_token->name), std::move(macro_token->params), std::move(body)));
} else if (auto filter_token = dynamic_cast<FilterTemplateToken*>(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<FilterNode>(token->location, std::move(filter_token->filter), std::move(body)));
} else if (dynamic_cast<CommentTemplateToken*>(token.get())) {
// Ignore comments
} else if (dynamic_cast<EndForTemplateToken*>(token.get())
|| dynamic_cast<EndSetTemplateToken*>(token.get())
|| dynamic_cast<EndMacroTemplateToken*>(token.get())
|| dynamic_cast<EndFilterTemplateToken*>(token.get())
|| dynamic_cast<EndIfTemplateToken*>(token.get())
|| dynamic_cast<ElseTemplateToken*>(token.get())
|| dynamic_cast<ElifTemplateToken*>(token.get())) {
Expand Down
5 changes: 5 additions & 0 deletions tests/test-syntax.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ TEST(SyntaxTest, SimpleCases) {
EXPECT_EQ(
"\r\nhey\r\nho!",
render("\r\n{{ 'hey\r\nho!' }}\r\n", {}, {}));
EXPECT_EQ(
"abc",
render("{% filter trim %} abc {% endfilter %}", {}, {}));

EXPECT_EQ(
"a\n b\n| a\n b\n",
Expand Down Expand Up @@ -385,11 +388,13 @@ TEST(SyntaxTest, SimpleCases) {
expect_throws_with_message_substr([]() { render("{% endif %}", {}, {}); }, "Unexpected endif");
expect_throws_with_message_substr([]() { render("{% elif 1 %}", {}, {}); }, "Unexpected elif");
expect_throws_with_message_substr([]() { render("{% endfor %}", {}, {}); }, "Unexpected endfor");
expect_throws_with_message_substr([]() { render("{% endfilter %}", {}, {}); }, "Unexpected endfilter");

expect_throws_with_message_substr([]() { render("{% if 1 %}", {}, {}); }, "Unterminated if");
expect_throws_with_message_substr([]() { render("{% for x in 1 %}", {}, {}); }, "Unterminated for");
expect_throws_with_message_substr([]() { render("{% if 1 %}{% else %}", {}, {}); }, "Unterminated if");
expect_throws_with_message_substr([]() { render("{% if 1 %}{% else %}{% elif 1 %}{% endif %}", {}, {}); }, "Unterminated if");
expect_throws_with_message_substr([]() { render("{% filter trim %}", {}, {}); }, "Unterminated filter");

EXPECT_EQ(
"3",
Expand Down
Loading