From 8aa138e5117ce08ac6fbd0b1da0680774fffcf25 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 6 Dec 2024 18:54:14 +0000 Subject: [PATCH] Add support for filter...endfilter blocks Add support for filter...endfilter blocks https://jinja.palletsprojects.com/en/stable/templates/#id11 Needed for https://github.com/google/minja/issues/7 --- README.md | 1 + include/minja/minja.hpp | 55 +++++++++++++++++++++++++++++++++++++++-- tests/test-syntax.cpp | 5 ++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b34284..3b77a9b 100644 --- a/README.md +++ b/README.md @@ -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): diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index 3b7e244..66bf536 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -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) { @@ -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"; } @@ -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 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; @@ -978,6 +990,29 @@ class MacroNode : public TemplateNode { } }; +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); + + 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 var_names; @@ -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]*([-~])?%\})"); @@ -2145,6 +2180,15 @@ class Parser { } else if (keyword == "endmacro") { auto post_space = parseBlockClose(); tokens.push_back(nonstd_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(nonstd_make_unique(location, pre_space, post_space, std::move(filter))); + } else if (keyword == "endfilter") { + auto post_space = parseBlockClose(); + tokens.push_back(nonstd_make_unique(location, pre_space, post_space)); } else { throw std::runtime_error("Unexpected block: " + keyword); } @@ -2241,11 +2285,18 @@ class Parser { 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())) { diff --git a/tests/test-syntax.cpp b/tests/test-syntax.cpp index 540ad83..ddb2e7c 100644 --- a/tests/test-syntax.cpp +++ b/tests/test-syntax.cpp @@ -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", @@ -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",