Add pipe syntax support for function calls like in Jinja2, resolves #294 (#296)

This commit is contained in:
Vladislav Nawrocki
2025-10-18 15:35:57 +03:00
committed by GitHub
parent b3d0e06a95
commit ea845eee91
6 changed files with 104 additions and 0 deletions

View File

@@ -270,6 +270,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 neighbour value
render("Hello {{ neighbour | 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.

View File

@@ -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 ')':

View File

@@ -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<FunctionNode>(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;
}

View File

@@ -44,6 +44,7 @@ struct Token {
GreaterEqual, // >=
LessThan, // <
LessEqual, // <=
Pipe, // |
Unknown,
Eof,
};

View File

@@ -1002,6 +1002,7 @@ struct Token {
GreaterEqual, // >=
LessThan, // <
LessEqual, // <=
Pipe, // |
Unknown,
Eof,
};
@@ -1138,6 +1139,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 ')':
@@ -1797,6 +1800,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<FunctionNode>(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;
}

View File

@@ -154,6 +154,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") {