From 18c7a49b97d0637743a043cb5e04bbc9a6a5a6e2 Mon Sep 17 00:00:00 2001 From: Vladislav Navrocky Date: Mon, 10 Feb 2025 00:59:29 +0300 Subject: [PATCH] Add pipe syntax support for function calls like in Jinja2, resolves #294 --- README.md | 10 ++++++++ include/inja/lexer.hpp | 2 ++ include/inja/parser.hpp | 41 +++++++++++++++++++++++++++++++++ include/inja/token.hpp | 1 + single_include/inja/inja.hpp | 44 ++++++++++++++++++++++++++++++++++++ test/test-renderer.cpp | 6 +++++ 6 files changed, 104 insertions(+) diff --git a/README.md b/README.md index c03f8419..1e6c074d 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,16 @@ render("{{ isArray(guests) }}", data); // "true" // Implemented type checks: isArray, isBoolean, isFloat, isInteger, isNumber, isObject, isString, ``` +The Jinja2 pipe call syntax of functions is also supported: + +```.cpp +// Upper neightbour value +render("Hello {{ neightbour | upper }}!", data); // "Hello PETER!" + +// Sort array and join with comma +render("{{ [\"B\", \"A\", \"C\"] | sort | join(\",\") }}", data); // "A,B,C" +``` + ### Callbacks You can create your own and more complex functions with callbacks. These are implemented with `std::function`, so you can for example use C++ lambdas. Inja `Arguments` are a vector of json pointers. diff --git a/include/inja/lexer.hpp b/include/inja/lexer.hpp index 6c744039..6127d449 100644 --- a/include/inja/lexer.hpp +++ b/include/inja/lexer.hpp @@ -115,6 +115,8 @@ class Lexer { return make_token(Token::Kind::Comma); case ':': return make_token(Token::Kind::Colon); + case '|': + return make_token(Token::Kind::Pipe); case '(': return make_token(Token::Kind::LeftParen); case ')': diff --git a/include/inja/parser.hpp b/include/inja/parser.hpp index 3c396c9b..ad493e48 100644 --- a/include/inja/parser.hpp +++ b/include/inja/parser.hpp @@ -350,6 +350,47 @@ class Parser { } arguments.emplace_back(expr); } break; + + // parse function call pipe syntax + case Token::Kind::Pipe: { + // get function name + get_next_token(); + if (tok.kind != Token::Kind::Id) { + throw_parser_error("expected function name, got '" + tok.describe() + "'"); + } + auto func = std::make_shared(tok.text, tok.text.data() - tmpl.content.c_str()); + // add first parameter as last value from arguments + func->number_args += 1; + func->arguments.emplace_back(arguments.back()); + arguments.pop_back(); + get_peek_token(); + if (peek_tok.kind == Token::Kind::LeftParen) { + get_next_token(); + // parse additional parameters + do { + get_next_token(); + auto expr = parse_expression(tmpl); + if (!expr) { + break; + } + func->number_args += 1; + func->arguments.emplace_back(expr); + } while (tok.kind == Token::Kind::Comma); + if (tok.kind != Token::Kind::RightParen) { + throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'"); + } + } + // search store for defined function with such name and number of args + auto function_data = function_storage.find_function(func->name, func->number_args); + if (function_data.operation == FunctionStorage::Operation::None) { + throw_parser_error("unknown function " + func->name); + } + func->operation = function_data.operation; + if (function_data.operation == FunctionStorage::Operation::Callback) { + func->callback = function_data.callback; + } + arguments.emplace_back(func); + } break; default: goto break_loop; } diff --git a/include/inja/token.hpp b/include/inja/token.hpp index 207e78e2..49a9e55e 100644 --- a/include/inja/token.hpp +++ b/include/inja/token.hpp @@ -44,6 +44,7 @@ struct Token { GreaterEqual, // >= LessThan, // < LessEqual, // <= + Pipe, // | Unknown, Eof, }; diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp index b751d6d8..3bcf2023 100644 --- a/single_include/inja/inja.hpp +++ b/single_include/inja/inja.hpp @@ -987,6 +987,7 @@ struct Token { GreaterEqual, // >= LessThan, // < LessEqual, // <= + Pipe, // | Unknown, Eof, }; @@ -1123,6 +1124,8 @@ class Lexer { return make_token(Token::Kind::Comma); case ':': return make_token(Token::Kind::Colon); + case '|': + return make_token(Token::Kind::Pipe); case '(': return make_token(Token::Kind::LeftParen); case ')': @@ -1781,6 +1784,47 @@ class Parser { } arguments.emplace_back(expr); } break; + + // parse function call pipe syntax + case Token::Kind::Pipe: { + // get function name + get_next_token(); + if (tok.kind != Token::Kind::Id) { + throw_parser_error("expected function name, got '" + tok.describe() + "'"); + } + auto func = std::make_shared(tok.text, tok.text.data() - tmpl.content.c_str()); + // add first parameter as last value from arguments + func->number_args += 1; + func->arguments.emplace_back(arguments.back()); + arguments.pop_back(); + get_peek_token(); + if (peek_tok.kind == Token::Kind::LeftParen) { + get_next_token(); + // parse additional parameters + do { + get_next_token(); + auto expr = parse_expression(tmpl); + if (!expr) { + break; + } + func->number_args += 1; + func->arguments.emplace_back(expr); + } while (tok.kind == Token::Kind::Comma); + if (tok.kind != Token::Kind::RightParen) { + throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'"); + } + } + // search store for defined function with such name and number of args + auto function_data = function_storage.find_function(func->name, func->number_args); + if (function_data.operation == FunctionStorage::Operation::None) { + throw_parser_error("unknown function " + func->name); + } + func->operation = function_data.operation; + if (function_data.operation == FunctionStorage::Operation::Callback) { + func->callback = function_data.callback; + } + arguments.emplace_back(func); + } break; default: goto break_loop; } diff --git a/test/test-renderer.cpp b/test/test-renderer.cpp index 41430f51..01a4725e 100644 --- a/test/test-renderer.cpp +++ b/test/test-renderer.cpp @@ -157,6 +157,12 @@ Yeah! data) == R""""(Yeah! )""""); } + + SUBCASE("pipe syntax") { + CHECK(env.render("{{ brother.name | upper }}", data) == "CHRIS"); + CHECK(env.render("{{ brother.name | upper | lower }}", data) == "chris"); + CHECK(env.render("{{ [\"C\", \"A\", \"B\"] | sort | join(\",\") }}", data) == "A,B,C"); + } } TEST_CASE("templates") {