mirror of
https://github.com/pantor/inja.git
synced 2026-02-17 09:03:58 +00:00
Rewarite core with an AST for statements and RPN for expressions (#149)
* test * improve ast * add if statement * shunting-yard start * renderer as node visitor * improve ast * improve ast further * first functions * improve ast v3 * improve ast v4 * fix parser error location * nested ifs * fix comma, activate more tests * fix line statements * fix some more tests * fix callbacks without arguments * add json literal array and object * use switch in expression * fix default function * fix loop data * improved tests and benchmark * fix minus numbers * improve all * fix warnings, optimizations * fix callbacks argument order * dont move loop parent * a few more test * fix clang-3 * fix pointers * clean * update single include
This commit is contained in:
@@ -76,11 +76,7 @@ execute_process(COMMAND scripts/update_single_include.sh WORKING_DIRECTORY ${PRO
|
||||
if(BUILD_TESTING AND INJA_BUILD_TESTS)
|
||||
enable_testing()
|
||||
|
||||
add_executable(inja_test
|
||||
test/unit.cpp
|
||||
test/unit-files.cpp
|
||||
test/unit-renderer.cpp
|
||||
)
|
||||
add_executable(inja_test test/test.cpp)
|
||||
target_link_libraries(inja_test PRIVATE inja)
|
||||
|
||||
add_test(inja_test ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/inja_test)
|
||||
@@ -90,11 +86,7 @@ if(BUILD_TESTING AND INJA_BUILD_TESTS)
|
||||
target_compile_features(single_inja INTERFACE cxx_std_11)
|
||||
target_include_directories(single_inja INTERFACE single_include include third_party/include)
|
||||
|
||||
add_executable(single_inja_test
|
||||
test/unit.cpp
|
||||
test/unit-files.cpp
|
||||
test/unit-renderer.cpp
|
||||
)
|
||||
add_executable(single_inja_test test/test.cpp)
|
||||
target_link_libraries(single_inja_test PRIVATE single_inja)
|
||||
|
||||
add_test(single_inja_test ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/single_inja_test)
|
||||
|
||||
20
README.md
20
README.md
@@ -109,10 +109,6 @@ Environment env_1 {"../path/templates/"};
|
||||
// With separate input and output path
|
||||
Environment env_2 {"../path/templates/", "../path/results/"};
|
||||
|
||||
// Choose between dot notation (like Jinja2) and JSON pointer to access elements
|
||||
env.set_element_notation(ElementNotation::Dot); // (default) e.g. time.start
|
||||
env.set_element_notation(ElementNotation::Pointer); // e.g. time/start
|
||||
|
||||
// With other opening and closing strings (here the defaults)
|
||||
env.set_expression("{{", "}}"); // Expressions
|
||||
env.set_comment("{#", "#}"); // Comments
|
||||
@@ -270,15 +266,15 @@ Stripping behind a statement also remove any newlines.
|
||||
|
||||
### Callbacks
|
||||
|
||||
You can create your own and more complex functions with 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.
|
||||
```.cpp
|
||||
Environment env;
|
||||
|
||||
/*
|
||||
* Callbacks are defined by its:
|
||||
* - name
|
||||
* - number of arguments
|
||||
* - callback function. Implemented with std::function, you can for example use lambdas.
|
||||
* - name,
|
||||
* - (optional) number of arguments,
|
||||
* - callback function.
|
||||
*/
|
||||
env.add_callback("double", 1, [](Arguments& args) {
|
||||
int number = args.at(0)->get<int>(); // Adapt the index and type of the argument
|
||||
@@ -288,6 +284,14 @@ env.add_callback("double", 1, [](Arguments& args) {
|
||||
// You can then use a callback like a regular function
|
||||
env.render("{{ double(16) }}", data); // "32"
|
||||
|
||||
// Inja falls back to variadic callbacks if the number of expected arguments is omitted.
|
||||
env.add_callback("argmax", [](Arguments& args) {
|
||||
auto result = std::max_element(args.begin(), args.end(), [](const json* a, const json* b) { return *a < *b;});
|
||||
return std::distance(args.begin(), result);
|
||||
});
|
||||
env.render("{{ argmax(4, 2, 6) }}", data); // "2"
|
||||
env.render("{{ argmax(0, 2, 6, 8, 3) }}", data); // "3"
|
||||
|
||||
// A callback without argument can be used like a dynamic variable:
|
||||
std::string greet = "Hello";
|
||||
env.add_callback("double-greetings", 0, [greet](Arguments args) {
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
|
||||
namespace inja {
|
||||
|
||||
enum class ElementNotation { Dot, Pointer };
|
||||
|
||||
/*!
|
||||
* \brief Class for lexer configuration.
|
||||
*/
|
||||
@@ -58,7 +56,6 @@ struct LexerConfig {
|
||||
* \brief Class for parser configuration.
|
||||
*/
|
||||
struct ParserConfig {
|
||||
ElementNotation notation {ElementNotation::Dot};
|
||||
bool search_included_templates_in_files {true};
|
||||
};
|
||||
|
||||
|
||||
@@ -84,11 +84,6 @@ public:
|
||||
lexer_config.lstrip_blocks = lstrip_blocks;
|
||||
}
|
||||
|
||||
/// Sets the element notation syntax
|
||||
void set_element_notation(ElementNotation notation) {
|
||||
parser_config.notation = notation;
|
||||
}
|
||||
|
||||
/// Sets the element notation syntax
|
||||
void set_search_included_templates_in_files(bool search_in_files) {
|
||||
parser_config.search_included_templates_in_files = search_in_files;
|
||||
@@ -100,12 +95,12 @@ public:
|
||||
}
|
||||
|
||||
Template parse(nonstd::string_view input) {
|
||||
Parser parser(parser_config, lexer_config, template_storage);
|
||||
Parser parser(parser_config, lexer_config, template_storage, function_storage);
|
||||
return parser.parse(input);
|
||||
}
|
||||
|
||||
Template parse_template(const std::string &filename) {
|
||||
Parser parser(parser_config, lexer_config, template_storage);
|
||||
Parser parser(parser_config, lexer_config, template_storage, function_storage);
|
||||
auto result = Template(parser.load_file(input_path + static_cast<std::string>(filename)));
|
||||
parser.parse_into_template(result, input_path + static_cast<std::string>(filename));
|
||||
return result;
|
||||
@@ -157,7 +152,7 @@ public:
|
||||
}
|
||||
|
||||
std::string load_file(const std::string &filename) {
|
||||
Parser parser(parser_config, lexer_config, template_storage);
|
||||
Parser parser(parser_config, lexer_config, template_storage, function_storage);
|
||||
return parser.load_file(input_path + filename);
|
||||
}
|
||||
|
||||
@@ -168,8 +163,18 @@ public:
|
||||
return j;
|
||||
}
|
||||
|
||||
void add_callback(const std::string &name, unsigned int numArgs, const CallbackFunction &callback) {
|
||||
function_storage.add_callback(name, numArgs, callback);
|
||||
/*!
|
||||
@brief Adds a variadic callback
|
||||
*/
|
||||
void add_callback(const std::string &name, const CallbackFunction &callback) {
|
||||
function_storage.add_callback(name, -1, callback);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief Adds a callback with given number or arguments
|
||||
*/
|
||||
void add_callback(const std::string &name, int num_args, const CallbackFunction &callback) {
|
||||
function_storage.add_callback(name, num_args, callback);
|
||||
}
|
||||
|
||||
/** Includes a template with a given name into the environment.
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
// Copyright (c) 2020 Pantor. All rights reserved.
|
||||
|
||||
#ifndef INCLUDE_INJA_FUNCTION_STORAGE_HPP_
|
||||
#define INCLUDE_INJA_FUNCTION_STORAGE_HPP_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "node.hpp"
|
||||
#include "string_view.hpp"
|
||||
|
||||
namespace inja {
|
||||
@@ -19,63 +18,116 @@ using CallbackFunction = std::function<json(Arguments &args)>;
|
||||
* \brief Class for builtin functions and user-defined callbacks.
|
||||
*/
|
||||
class FunctionStorage {
|
||||
struct FunctionData {
|
||||
unsigned int num_args {0};
|
||||
Node::Op op {Node::Op::Nop}; // for builtins
|
||||
CallbackFunction function; // for callbacks
|
||||
public:
|
||||
enum class Operation {
|
||||
Not,
|
||||
And,
|
||||
Or,
|
||||
In,
|
||||
Equal,
|
||||
NotEqual,
|
||||
Greater,
|
||||
GreaterEqual,
|
||||
Less,
|
||||
LessEqual,
|
||||
Add,
|
||||
Subtract,
|
||||
Multiplication,
|
||||
Division,
|
||||
Power,
|
||||
Modulo,
|
||||
At,
|
||||
Default,
|
||||
DivisibleBy,
|
||||
Even,
|
||||
Exists,
|
||||
ExistsInObject,
|
||||
First,
|
||||
Float,
|
||||
Int,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsFloat,
|
||||
IsInteger,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsString,
|
||||
Last,
|
||||
Length,
|
||||
Lower,
|
||||
Max,
|
||||
Min,
|
||||
Odd,
|
||||
Range,
|
||||
Round,
|
||||
Sort,
|
||||
Upper,
|
||||
Callback,
|
||||
ParenLeft,
|
||||
ParenRight,
|
||||
None,
|
||||
};
|
||||
|
||||
std::map<std::string, std::vector<FunctionData>> storage;
|
||||
const int VARIADIC {-1};
|
||||
|
||||
FunctionData &get_or_new(nonstd::string_view name, unsigned int num_args) {
|
||||
auto &vec = storage[static_cast<std::string>(name)];
|
||||
for (auto &i : vec) {
|
||||
if (i.num_args == num_args) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
vec.emplace_back();
|
||||
vec.back().num_args = num_args;
|
||||
return vec.back();
|
||||
}
|
||||
struct FunctionData {
|
||||
Operation operation;
|
||||
|
||||
const FunctionData *get(nonstd::string_view name, unsigned int num_args) const {
|
||||
auto it = storage.find(static_cast<std::string>(name));
|
||||
if (it == storage.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
CallbackFunction callback;
|
||||
};
|
||||
|
||||
for (auto &&i : it->second) {
|
||||
if (i.num_args == num_args) {
|
||||
return &i;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
std::map<std::pair<std::string, int>, FunctionData> function_storage = {
|
||||
{std::make_pair("at", 2), FunctionData { Operation::At }},
|
||||
{std::make_pair("default", 2), FunctionData { Operation::Default }},
|
||||
{std::make_pair("divisibleBy", 2), FunctionData { Operation::DivisibleBy }},
|
||||
{std::make_pair("even", 1), FunctionData { Operation::Even }},
|
||||
{std::make_pair("exists", 1), FunctionData { Operation::Exists }},
|
||||
{std::make_pair("existsIn", 2), FunctionData { Operation::ExistsInObject }},
|
||||
{std::make_pair("first", 1), FunctionData { Operation::First }},
|
||||
{std::make_pair("float", 1), FunctionData { Operation::Float }},
|
||||
{std::make_pair("int", 1), FunctionData { Operation::Int }},
|
||||
{std::make_pair("isArray", 1), FunctionData { Operation::IsArray }},
|
||||
{std::make_pair("isBoolean", 1), FunctionData { Operation::IsBoolean }},
|
||||
{std::make_pair("isFloat", 1), FunctionData { Operation::IsFloat }},
|
||||
{std::make_pair("isInteger", 1), FunctionData { Operation::IsInteger }},
|
||||
{std::make_pair("isNumber", 1), FunctionData { Operation::IsNumber }},
|
||||
{std::make_pair("isObject", 1), FunctionData { Operation::IsObject }},
|
||||
{std::make_pair("isString", 1), FunctionData { Operation::IsString }},
|
||||
{std::make_pair("last", 1), FunctionData { Operation::Last }},
|
||||
{std::make_pair("length", 1), FunctionData { Operation::Length }},
|
||||
{std::make_pair("lower", 1), FunctionData { Operation::Lower }},
|
||||
{std::make_pair("max", 1), FunctionData { Operation::Max }},
|
||||
{std::make_pair("min", 1), FunctionData { Operation::Min }},
|
||||
{std::make_pair("odd", 1), FunctionData { Operation::Odd }},
|
||||
{std::make_pair("range", 1), FunctionData { Operation::Range }},
|
||||
{std::make_pair("round", 2), FunctionData { Operation::Round }},
|
||||
{std::make_pair("sort", 1), FunctionData { Operation::Sort }},
|
||||
{std::make_pair("upper", 1), FunctionData { Operation::Upper }},
|
||||
};
|
||||
|
||||
public:
|
||||
void add_builtin(nonstd::string_view name, unsigned int num_args, Node::Op op) {
|
||||
auto &data = get_or_new(name, num_args);
|
||||
data.op = op;
|
||||
void add_builtin(nonstd::string_view name, int num_args, Operation op) {
|
||||
function_storage.emplace(std::make_pair(static_cast<std::string>(name), num_args), FunctionData { op });
|
||||
}
|
||||
|
||||
void add_callback(nonstd::string_view name, unsigned int num_args, const CallbackFunction &function) {
|
||||
auto &data = get_or_new(name, num_args);
|
||||
data.function = function;
|
||||
void add_callback(nonstd::string_view name, int num_args, const CallbackFunction &callback) {
|
||||
function_storage.emplace(std::make_pair(static_cast<std::string>(name), num_args), FunctionData { Operation::Callback, callback });
|
||||
}
|
||||
|
||||
Node::Op find_builtin(nonstd::string_view name, unsigned int num_args) const {
|
||||
if (auto ptr = get(name, num_args)) {
|
||||
return ptr->op;
|
||||
FunctionData find_function(nonstd::string_view name, int num_args) const {
|
||||
auto it = function_storage.find(std::make_pair(static_cast<std::string>(name), num_args));
|
||||
if (it != function_storage.end()) {
|
||||
return it->second;
|
||||
|
||||
// Find variadic function
|
||||
} else if (num_args > 0) {
|
||||
it = function_storage.find(std::make_pair(static_cast<std::string>(name), VARIADIC));
|
||||
if (it != function_storage.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
return Node::Op::Nop;
|
||||
}
|
||||
|
||||
CallbackFunction find_callback(nonstd::string_view name, unsigned int num_args) const {
|
||||
if (auto ptr = get(name, num_args)) {
|
||||
return ptr->function;
|
||||
}
|
||||
return nullptr;
|
||||
return { Operation::None };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#ifndef INCLUDE_INJA_INJA_HPP_
|
||||
#define INCLUDE_INJA_INJA_HPP_
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "environment.hpp"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
// Copyright (c) 2020 Pantor. All rights reserved.
|
||||
|
||||
#ifndef INCLUDE_INJA_LEXER_HPP_
|
||||
#define INCLUDE_INJA_LEXER_HPP_
|
||||
@@ -27,12 +27,18 @@ class Lexer {
|
||||
StatementStartForceLstrip,
|
||||
StatementBody,
|
||||
CommentStart,
|
||||
CommentBody
|
||||
CommentBody,
|
||||
};
|
||||
|
||||
|
||||
enum class MinusState {
|
||||
Operator,
|
||||
Number,
|
||||
};
|
||||
|
||||
const LexerConfig &config;
|
||||
|
||||
State state;
|
||||
MinusState minus_state;
|
||||
nonstd::string_view m_in;
|
||||
size_t tok_start;
|
||||
size_t pos;
|
||||
@@ -77,10 +83,31 @@ class Lexer {
|
||||
|
||||
pos = tok_start + 1;
|
||||
if (std::isalpha(ch)) {
|
||||
minus_state = MinusState::Operator;
|
||||
return scan_id();
|
||||
}
|
||||
|
||||
MinusState current_minus_state = minus_state;
|
||||
if (minus_state == MinusState::Operator) {
|
||||
minus_state = MinusState::Number;
|
||||
}
|
||||
|
||||
switch (ch) {
|
||||
case '+':
|
||||
return make_token(Token::Kind::Plus);
|
||||
case '-':
|
||||
if (current_minus_state == MinusState::Operator) {
|
||||
return make_token(Token::Kind::Minus);
|
||||
}
|
||||
return scan_number();
|
||||
case '*':
|
||||
return make_token(Token::Kind::Times);
|
||||
case '/':
|
||||
return make_token(Token::Kind::Slash);
|
||||
case '^':
|
||||
return make_token(Token::Kind::Power);
|
||||
case '%':
|
||||
return make_token(Token::Kind::Percent);
|
||||
case ',':
|
||||
return make_token(Token::Kind::Comma);
|
||||
case ':':
|
||||
@@ -88,14 +115,17 @@ class Lexer {
|
||||
case '(':
|
||||
return make_token(Token::Kind::LeftParen);
|
||||
case ')':
|
||||
minus_state = MinusState::Operator;
|
||||
return make_token(Token::Kind::RightParen);
|
||||
case '[':
|
||||
return make_token(Token::Kind::LeftBracket);
|
||||
case ']':
|
||||
minus_state = MinusState::Operator;
|
||||
return make_token(Token::Kind::RightBracket);
|
||||
case '{':
|
||||
return make_token(Token::Kind::LeftBrace);
|
||||
case '}':
|
||||
minus_state = MinusState::Operator;
|
||||
return make_token(Token::Kind::RightBrace);
|
||||
case '>':
|
||||
if (pos < m_in.size() && m_in[pos] == '=') {
|
||||
@@ -133,9 +163,10 @@ class Lexer {
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
case '-':
|
||||
minus_state = MinusState::Operator;
|
||||
return scan_number();
|
||||
case '_':
|
||||
minus_state = MinusState::Operator;
|
||||
return scan_id();
|
||||
default:
|
||||
return make_token(Token::Kind::Unknown);
|
||||
@@ -246,6 +277,7 @@ public:
|
||||
tok_start = 0;
|
||||
pos = 0;
|
||||
state = State::Text;
|
||||
minus_state = MinusState::Number;
|
||||
}
|
||||
|
||||
Token scan() {
|
||||
@@ -255,7 +287,7 @@ public:
|
||||
if (tok_start >= m_in.size()) {
|
||||
return make_token(Token::Kind::Eof);
|
||||
}
|
||||
|
||||
|
||||
switch (state) {
|
||||
default:
|
||||
case State::Text: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
// Copyright (c) 2020 Pantor. All rights reserved.
|
||||
|
||||
#ifndef INCLUDE_INJA_NODE_HPP_
|
||||
#define INCLUDE_INJA_NODE_HPP_
|
||||
@@ -8,122 +8,296 @@
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "function_storage.hpp"
|
||||
#include "string_view.hpp"
|
||||
|
||||
|
||||
namespace inja {
|
||||
|
||||
using json = nlohmann::json;
|
||||
class NodeVisitor;
|
||||
class BlockNode;
|
||||
class TextNode;
|
||||
class ExpressionNode;
|
||||
class LiteralNode;
|
||||
class JsonNode;
|
||||
class FunctionNode;
|
||||
class ExpressionListNode;
|
||||
class StatementNode;
|
||||
class ForStatementNode;
|
||||
class ForArrayStatementNode;
|
||||
class ForObjectStatementNode;
|
||||
class IfStatementNode;
|
||||
class IncludeStatementNode;
|
||||
|
||||
struct Node {
|
||||
enum class Op : uint8_t {
|
||||
Nop,
|
||||
// print StringRef (always immediate)
|
||||
PrintText,
|
||||
// print value
|
||||
PrintValue,
|
||||
// push value onto stack (always immediate)
|
||||
Push,
|
||||
|
||||
// builtin functions
|
||||
// result is pushed to stack
|
||||
// args specify number of arguments
|
||||
// all functions can take their "last" argument either immediate
|
||||
// or popped off stack (e.g. if immediate, it's like the immediate was
|
||||
// just pushed to the stack)
|
||||
Not,
|
||||
And,
|
||||
Or,
|
||||
In,
|
||||
Equal,
|
||||
Greater,
|
||||
GreaterEqual,
|
||||
Less,
|
||||
LessEqual,
|
||||
At,
|
||||
Different,
|
||||
DivisibleBy,
|
||||
Even,
|
||||
First,
|
||||
Float,
|
||||
Int,
|
||||
Last,
|
||||
Length,
|
||||
Lower,
|
||||
Max,
|
||||
Min,
|
||||
Odd,
|
||||
Range,
|
||||
Result,
|
||||
Round,
|
||||
Sort,
|
||||
Upper,
|
||||
Exists,
|
||||
ExistsInObject,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsInteger,
|
||||
IsFloat,
|
||||
IsObject,
|
||||
IsArray,
|
||||
IsString,
|
||||
Default,
|
||||
class NodeVisitor {
|
||||
public:
|
||||
virtual void visit(const BlockNode& node) = 0;
|
||||
virtual void visit(const TextNode& node) = 0;
|
||||
virtual void visit(const ExpressionNode& node) = 0;
|
||||
virtual void visit(const LiteralNode& node) = 0;
|
||||
virtual void visit(const JsonNode& node) = 0;
|
||||
virtual void visit(const FunctionNode& node) = 0;
|
||||
virtual void visit(const ExpressionListNode& node) = 0;
|
||||
virtual void visit(const StatementNode& node) = 0;
|
||||
virtual void visit(const ForStatementNode& node) = 0;
|
||||
virtual void visit(const ForArrayStatementNode& node) = 0;
|
||||
virtual void visit(const ForObjectStatementNode& node) = 0;
|
||||
virtual void visit(const IfStatementNode& node) = 0;
|
||||
virtual void visit(const IncludeStatementNode& node) = 0;
|
||||
};
|
||||
|
||||
// include another template
|
||||
// value is the template name
|
||||
Include,
|
||||
|
||||
// callback function
|
||||
// str is the function name (this means it cannot be a lookup)
|
||||
// args specify number of arguments
|
||||
// as with builtin functions, "last" argument can be immediate
|
||||
Callback,
|
||||
class AstNode {
|
||||
public:
|
||||
virtual void accept(NodeVisitor& v) const = 0;
|
||||
|
||||
// unconditional jump
|
||||
// args is the index of the node to jump to.
|
||||
Jump,
|
||||
|
||||
// conditional jump
|
||||
// value popped off stack is checked for truthyness
|
||||
// if false, args is the index of the node to jump to.
|
||||
// if true, no action is taken (falls through)
|
||||
ConditionalJump,
|
||||
|
||||
// start loop
|
||||
// value popped off stack is what is iterated over
|
||||
// args is index of node after end loop (jumped to if iterable is empty)
|
||||
// immediate value is key name (for maps)
|
||||
// str is value name
|
||||
StartLoop,
|
||||
|
||||
// end a loop
|
||||
// args is index of the first node in the loop body
|
||||
EndLoop,
|
||||
};
|
||||
|
||||
enum Flag {
|
||||
// location of value for value-taking ops (mask)
|
||||
ValueMask = 0x03,
|
||||
// pop value off stack
|
||||
ValuePop = 0x00,
|
||||
// value is immediate rather than on stack
|
||||
ValueImmediate = 0x01,
|
||||
// lookup immediate str (dot notation)
|
||||
ValueLookupDot = 0x02,
|
||||
// lookup immediate str (json pointer notation)
|
||||
ValueLookupPointer = 0x03,
|
||||
};
|
||||
|
||||
Op op {Op::Nop};
|
||||
uint32_t args : 30;
|
||||
uint32_t flags : 2;
|
||||
|
||||
json value;
|
||||
std::string str;
|
||||
size_t pos;
|
||||
|
||||
explicit Node(Op op, unsigned int args, size_t pos) : op(op), args(args), flags(0), pos(pos) {}
|
||||
explicit Node(Op op, nonstd::string_view str, unsigned int flags, size_t pos) : op(op), args(0), flags(flags), str(str), pos(pos) {}
|
||||
explicit Node(Op op, json &&value, unsigned int flags, size_t pos) : op(op), args(0), flags(flags), value(std::move(value)), pos(pos) {}
|
||||
AstNode(size_t pos) : pos(pos) { }
|
||||
virtual ~AstNode() { };
|
||||
};
|
||||
|
||||
|
||||
class BlockNode : public AstNode {
|
||||
public:
|
||||
std::vector<std::shared_ptr<AstNode>> nodes;
|
||||
|
||||
explicit BlockNode() : AstNode(0) {}
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class TextNode : public AstNode {
|
||||
public:
|
||||
std::string content;
|
||||
|
||||
explicit TextNode(nonstd::string_view content, size_t pos): AstNode(pos), content(content) { }
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class ExpressionNode : public AstNode {
|
||||
public:
|
||||
explicit ExpressionNode(size_t pos) : AstNode(pos) {}
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class LiteralNode : public ExpressionNode {
|
||||
public:
|
||||
nlohmann::json value;
|
||||
|
||||
explicit LiteralNode(const nlohmann::json& value, size_t pos) : ExpressionNode(pos), value(value) { }
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class JsonNode : public ExpressionNode {
|
||||
public:
|
||||
std::string name;
|
||||
std::string ptr {""};
|
||||
|
||||
explicit JsonNode(nonstd::string_view ptr_name, size_t pos) : ExpressionNode(pos), name(ptr_name) {
|
||||
// Convert dot notation to json pointer notation
|
||||
do {
|
||||
nonstd::string_view part;
|
||||
std::tie(part, ptr_name) = string_view::split(ptr_name, '.');
|
||||
ptr.push_back('/');
|
||||
ptr.append(part.begin(), part.end());
|
||||
} while (!ptr_name.empty());
|
||||
}
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class FunctionNode : public ExpressionNode {
|
||||
using Op = FunctionStorage::Operation;
|
||||
|
||||
public:
|
||||
enum class Associativity {
|
||||
Left,
|
||||
Right,
|
||||
};
|
||||
|
||||
unsigned int precedence;
|
||||
Associativity associativity;
|
||||
|
||||
Op operation;
|
||||
|
||||
std::string name;
|
||||
size_t number_args;
|
||||
CallbackFunction callback;
|
||||
|
||||
explicit FunctionNode(nonstd::string_view name, size_t pos) : ExpressionNode(pos), precedence(5), associativity(Associativity::Left), operation(Op::Callback), name(name), number_args(1) { }
|
||||
explicit FunctionNode(Op operation, size_t pos) : ExpressionNode(pos), operation(operation), number_args(1) {
|
||||
switch (operation) {
|
||||
case Op::Not: {
|
||||
precedence = 4;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::And: {
|
||||
precedence = 1;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::Or: {
|
||||
precedence = 1;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::In: {
|
||||
precedence = 2;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::Equal: {
|
||||
precedence = 2;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::NotEqual: {
|
||||
precedence = 2;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::Greater: {
|
||||
precedence = 2;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::GreaterEqual: {
|
||||
precedence = 2;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::Less: {
|
||||
precedence = 2;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::LessEqual: {
|
||||
precedence = 2;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::Add: {
|
||||
precedence = 3;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::Subtract: {
|
||||
precedence = 3;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::Multiplication: {
|
||||
precedence = 4;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::Division: {
|
||||
precedence = 4;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
case Op::Power: {
|
||||
precedence = 5;
|
||||
associativity = Associativity::Right;
|
||||
} break;
|
||||
case Op::Modulo: {
|
||||
precedence = 4;
|
||||
associativity = Associativity::Left;
|
||||
} break;
|
||||
default: {
|
||||
precedence = 1;
|
||||
associativity = Associativity::Left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class ExpressionListNode : public AstNode {
|
||||
public:
|
||||
std::vector<std::shared_ptr<ExpressionNode>> rpn_output;
|
||||
|
||||
explicit ExpressionListNode() : AstNode(0) { }
|
||||
explicit ExpressionListNode(size_t pos) : AstNode(pos) { }
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class StatementNode : public AstNode {
|
||||
public:
|
||||
StatementNode(size_t pos) : AstNode(pos) { }
|
||||
|
||||
virtual void accept(NodeVisitor& v) const = 0;
|
||||
};
|
||||
|
||||
class ForStatementNode : public StatementNode {
|
||||
public:
|
||||
ExpressionListNode condition;
|
||||
BlockNode body;
|
||||
BlockNode *parent;
|
||||
|
||||
ForStatementNode(size_t pos) : StatementNode(pos) { }
|
||||
|
||||
virtual void accept(NodeVisitor& v) const = 0;
|
||||
};
|
||||
|
||||
class ForArrayStatementNode : public ForStatementNode {
|
||||
public:
|
||||
nonstd::string_view value;
|
||||
|
||||
explicit ForArrayStatementNode(nonstd::string_view value, size_t pos) : ForStatementNode(pos), value(value) { }
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class ForObjectStatementNode : public ForStatementNode {
|
||||
public:
|
||||
nonstd::string_view key;
|
||||
nonstd::string_view value;
|
||||
|
||||
explicit ForObjectStatementNode(nonstd::string_view key, nonstd::string_view value, size_t pos) : ForStatementNode(pos), key(key), value(value) { }
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class IfStatementNode : public StatementNode {
|
||||
public:
|
||||
ExpressionListNode condition;
|
||||
BlockNode true_statement;
|
||||
BlockNode false_statement;
|
||||
BlockNode *parent;
|
||||
|
||||
bool is_nested;
|
||||
bool has_false_statement {false};
|
||||
|
||||
explicit IfStatementNode(size_t pos) : StatementNode(pos), is_nested(false) { }
|
||||
explicit IfStatementNode(bool is_nested, size_t pos) : StatementNode(pos), is_nested(is_nested) { }
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class IncludeStatementNode : public StatementNode {
|
||||
public:
|
||||
std::string file;
|
||||
|
||||
explicit IncludeStatementNode(const std::string& file, size_t pos) : StatementNode(pos), file(file) { }
|
||||
|
||||
void accept(NodeVisitor& v) const {
|
||||
v.visit(*this);
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace inja
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
// Copyright (c) 2020 Pantor. All rights reserved.
|
||||
|
||||
#ifndef INCLUDE_INJA_PARSER_HPP_
|
||||
#define INCLUDE_INJA_PARSER_HPP_
|
||||
|
||||
#include <limits>
|
||||
#include <stack>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
#include "config.hpp"
|
||||
#include "exceptions.hpp"
|
||||
#include "function_storage.hpp"
|
||||
#include "lexer.hpp"
|
||||
#include "template.hpp"
|
||||
#include "node.hpp"
|
||||
#include "template.hpp"
|
||||
#include "token.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
@@ -21,73 +23,32 @@
|
||||
|
||||
namespace inja {
|
||||
|
||||
class ParserStatic {
|
||||
ParserStatic() {
|
||||
function_storage.add_builtin("at", 2, Node::Op::At);
|
||||
function_storage.add_builtin("default", 2, Node::Op::Default);
|
||||
function_storage.add_builtin("divisibleBy", 2, Node::Op::DivisibleBy);
|
||||
function_storage.add_builtin("even", 1, Node::Op::Even);
|
||||
function_storage.add_builtin("first", 1, Node::Op::First);
|
||||
function_storage.add_builtin("float", 1, Node::Op::Float);
|
||||
function_storage.add_builtin("int", 1, Node::Op::Int);
|
||||
function_storage.add_builtin("last", 1, Node::Op::Last);
|
||||
function_storage.add_builtin("length", 1, Node::Op::Length);
|
||||
function_storage.add_builtin("lower", 1, Node::Op::Lower);
|
||||
function_storage.add_builtin("max", 1, Node::Op::Max);
|
||||
function_storage.add_builtin("min", 1, Node::Op::Min);
|
||||
function_storage.add_builtin("odd", 1, Node::Op::Odd);
|
||||
function_storage.add_builtin("range", 1, Node::Op::Range);
|
||||
function_storage.add_builtin("round", 2, Node::Op::Round);
|
||||
function_storage.add_builtin("sort", 1, Node::Op::Sort);
|
||||
function_storage.add_builtin("upper", 1, Node::Op::Upper);
|
||||
function_storage.add_builtin("exists", 1, Node::Op::Exists);
|
||||
function_storage.add_builtin("existsIn", 2, Node::Op::ExistsInObject);
|
||||
function_storage.add_builtin("isBoolean", 1, Node::Op::IsBoolean);
|
||||
function_storage.add_builtin("isNumber", 1, Node::Op::IsNumber);
|
||||
function_storage.add_builtin("isInteger", 1, Node::Op::IsInteger);
|
||||
function_storage.add_builtin("isFloat", 1, Node::Op::IsFloat);
|
||||
function_storage.add_builtin("isObject", 1, Node::Op::IsObject);
|
||||
function_storage.add_builtin("isArray", 1, Node::Op::IsArray);
|
||||
function_storage.add_builtin("isString", 1, Node::Op::IsString);
|
||||
}
|
||||
|
||||
public:
|
||||
ParserStatic(const ParserStatic &) = delete;
|
||||
ParserStatic &operator=(const ParserStatic &) = delete;
|
||||
|
||||
static const ParserStatic &get_instance() {
|
||||
static ParserStatic instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
FunctionStorage function_storage;
|
||||
};
|
||||
|
||||
|
||||
/*!
|
||||
* \brief Class for parsing an inja Template.
|
||||
*/
|
||||
class Parser {
|
||||
struct IfData {
|
||||
using jump_t = size_t;
|
||||
jump_t prev_cond_jump;
|
||||
std::vector<jump_t> uncond_jumps;
|
||||
|
||||
explicit IfData(jump_t condJump) : prev_cond_jump(condJump) {}
|
||||
};
|
||||
|
||||
|
||||
const ParserStatic &parser_static;
|
||||
const ParserConfig &config;
|
||||
|
||||
Lexer lexer;
|
||||
TemplateStorage &template_storage;
|
||||
const FunctionStorage &function_storage;
|
||||
|
||||
Token tok;
|
||||
Token peek_tok;
|
||||
Token tok, peek_tok;
|
||||
bool have_peek_tok {false};
|
||||
|
||||
std::vector<IfData> if_stack;
|
||||
std::vector<size_t> loop_stack;
|
||||
size_t current_paren_level {0};
|
||||
size_t current_bracket_level {0};
|
||||
size_t current_brace_level {0};
|
||||
|
||||
nonstd::string_view json_literal_start;
|
||||
|
||||
BlockNode *current_block {nullptr};
|
||||
ExpressionListNode *current_expression_list {nullptr};
|
||||
std::stack<std::pair<FunctionNode*, size_t>> function_stack;
|
||||
|
||||
std::stack<std::shared_ptr<FunctionNode>> operator_stack;
|
||||
std::stack<IfStatementNode*> if_statement_stack;
|
||||
std::stack<ForStatementNode*> for_statement_stack;
|
||||
|
||||
void throw_parser_error(const std::string &message) {
|
||||
throw ParserError(message, lexer.current_position());
|
||||
@@ -109,240 +70,245 @@ class Parser {
|
||||
}
|
||||
}
|
||||
|
||||
void add_json_literal(const char* content_ptr) {
|
||||
nonstd::string_view json_text(json_literal_start.data(), tok.text.data() - json_literal_start.data() + tok.text.size());
|
||||
current_expression_list->rpn_output.emplace_back(std::make_shared<LiteralNode>(json::parse(json_text), json_text.data() - content_ptr));
|
||||
}
|
||||
|
||||
|
||||
public:
|
||||
explicit Parser(const ParserConfig &parser_config, const LexerConfig &lexer_config,
|
||||
TemplateStorage &included_templates)
|
||||
: config(parser_config), lexer(lexer_config), template_storage(included_templates),
|
||||
parser_static(ParserStatic::get_instance()) {}
|
||||
TemplateStorage &template_storage, const FunctionStorage &function_storage)
|
||||
: config(parser_config), lexer(lexer_config), template_storage(template_storage), function_storage(function_storage) { }
|
||||
|
||||
bool parse_expression(Template &tmpl) {
|
||||
if (!parse_expression_and(tmpl)) {
|
||||
return false;
|
||||
}
|
||||
if (tok.kind != Token::Kind::Id || tok.text != static_cast<decltype(tok.text)>("or")) {
|
||||
return true;
|
||||
}
|
||||
get_next_token();
|
||||
if (!parse_expression_and(tmpl)) {
|
||||
return false;
|
||||
}
|
||||
append_function(tmpl, Node::Op::Or, 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_expression_and(Template &tmpl) {
|
||||
if (!parse_expression_not(tmpl)) {
|
||||
return false;
|
||||
}
|
||||
if (tok.kind != Token::Kind::Id || tok.text != static_cast<decltype(tok.text)>("and")) {
|
||||
return true;
|
||||
}
|
||||
get_next_token();
|
||||
if (!parse_expression_not(tmpl)) {
|
||||
return false;
|
||||
}
|
||||
append_function(tmpl, Node::Op::And, 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_expression_not(Template &tmpl) {
|
||||
if (tok.kind == Token::Kind::Id && tok.text == static_cast<decltype(tok.text)>("not")) {
|
||||
get_next_token();
|
||||
if (!parse_expression_not(tmpl)) {
|
||||
return false;
|
||||
}
|
||||
append_function(tmpl, Node::Op::Not, 1);
|
||||
return true;
|
||||
} else {
|
||||
return parse_expression_comparison(tmpl);
|
||||
}
|
||||
}
|
||||
|
||||
bool parse_expression_comparison(Template &tmpl) {
|
||||
if (!parse_expression_datum(tmpl)) {
|
||||
return false;
|
||||
}
|
||||
Node::Op op;
|
||||
switch (tok.kind) {
|
||||
case Token::Kind::Id:
|
||||
if (tok.text == static_cast<decltype(tok.text)>("in")) {
|
||||
op = Node::Op::In;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case Token::Kind::Equal:
|
||||
op = Node::Op::Equal;
|
||||
break;
|
||||
case Token::Kind::GreaterThan:
|
||||
op = Node::Op::Greater;
|
||||
break;
|
||||
case Token::Kind::LessThan:
|
||||
op = Node::Op::Less;
|
||||
break;
|
||||
case Token::Kind::LessEqual:
|
||||
op = Node::Op::LessEqual;
|
||||
break;
|
||||
case Token::Kind::GreaterEqual:
|
||||
op = Node::Op::GreaterEqual;
|
||||
break;
|
||||
case Token::Kind::NotEqual:
|
||||
op = Node::Op::Different;
|
||||
break;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
get_next_token();
|
||||
if (!parse_expression_datum(tmpl)) {
|
||||
return false;
|
||||
}
|
||||
append_function(tmpl, op, 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_expression_datum(Template &tmpl) {
|
||||
nonstd::string_view json_first;
|
||||
size_t bracket_level = 0;
|
||||
size_t brace_level = 0;
|
||||
|
||||
for (;;) {
|
||||
bool parse_expression(Template &tmpl, Token::Kind closing) {
|
||||
while (tok.kind != closing && tok.kind != Token::Kind::Eof) {
|
||||
// Literals
|
||||
switch (tok.kind) {
|
||||
case Token::Kind::LeftParen: {
|
||||
get_next_token();
|
||||
if (!parse_expression(tmpl)) {
|
||||
return false;
|
||||
case Token::Kind::String: {
|
||||
if (current_brace_level == 0 && current_bracket_level == 0) {
|
||||
json_literal_start = tok.text;
|
||||
add_json_literal(tmpl.content.c_str());
|
||||
}
|
||||
if (tok.kind != Token::Kind::RightParen) {
|
||||
throw_parser_error("unmatched '('");
|
||||
}
|
||||
get_next_token();
|
||||
return true;
|
||||
}
|
||||
case Token::Kind::Id:
|
||||
get_peek_token();
|
||||
if (peek_tok.kind == Token::Kind::LeftParen) {
|
||||
// function call, parse arguments
|
||||
Token func_token = tok;
|
||||
get_next_token(); // id
|
||||
get_next_token(); // leftParen
|
||||
unsigned int num_args = 0;
|
||||
if (tok.kind == Token::Kind::RightParen) {
|
||||
// no args
|
||||
get_next_token();
|
||||
} else {
|
||||
for (;;) {
|
||||
if (!parse_expression(tmpl)) {
|
||||
throw_parser_error("expected expression, got '" + tok.describe() + "'");
|
||||
}
|
||||
num_args += 1;
|
||||
if (tok.kind == Token::Kind::RightParen) {
|
||||
get_next_token();
|
||||
break;
|
||||
}
|
||||
if (tok.kind != Token::Kind::Comma) {
|
||||
throw_parser_error("expected ')' or ',', got '" + tok.describe() + "'");
|
||||
}
|
||||
get_next_token();
|
||||
}
|
||||
}
|
||||
|
||||
auto op = parser_static.function_storage.find_builtin(func_token.text, num_args);
|
||||
} break;
|
||||
case Token::Kind::Number: {
|
||||
if (current_brace_level == 0 && current_bracket_level == 0) {
|
||||
json_literal_start = tok.text;
|
||||
add_json_literal(tmpl.content.c_str());
|
||||
}
|
||||
|
||||
if (op != Node::Op::Nop) {
|
||||
// swap arguments for default(); see comment in RenderTo()
|
||||
if (op == Node::Op::Default) {
|
||||
std::swap(tmpl.nodes.back(), *(tmpl.nodes.rbegin() + 1));
|
||||
}
|
||||
append_function(tmpl, op, num_args);
|
||||
return true;
|
||||
} else {
|
||||
append_callback(tmpl, func_token.text, num_args);
|
||||
return true;
|
||||
}
|
||||
} else if (tok.text == static_cast<decltype(tok.text)>("true") ||
|
||||
tok.text == static_cast<decltype(tok.text)>("false") ||
|
||||
tok.text == static_cast<decltype(tok.text)>("null")) {
|
||||
// true, false, null are json literals
|
||||
if (brace_level == 0 && bracket_level == 0) {
|
||||
json_first = tok.text;
|
||||
goto returnJson;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
// normal literal (json read)
|
||||
} break;
|
||||
case Token::Kind::LeftBracket: {
|
||||
if (current_brace_level == 0 && current_bracket_level == 0) {
|
||||
json_literal_start = tok.text;
|
||||
}
|
||||
current_bracket_level += 1;
|
||||
|
||||
auto flag = config.notation == ElementNotation::Pointer ? Node::Flag::ValueLookupPointer : Node::Flag::ValueLookupDot;
|
||||
tmpl.nodes.emplace_back(Node::Op::Push, tok.text, flag, tok.text.data() - tmpl.content.c_str());
|
||||
get_next_token();
|
||||
return true;
|
||||
} break;
|
||||
case Token::Kind::LeftBrace: {
|
||||
if (current_brace_level == 0 && current_bracket_level == 0) {
|
||||
json_literal_start = tok.text;
|
||||
}
|
||||
// json passthrough
|
||||
case Token::Kind::Number:
|
||||
case Token::Kind::String:
|
||||
if (brace_level == 0 && bracket_level == 0) {
|
||||
json_first = tok.text;
|
||||
goto returnJson;
|
||||
}
|
||||
break;
|
||||
case Token::Kind::Comma:
|
||||
case Token::Kind::Colon:
|
||||
if (brace_level == 0 && bracket_level == 0) {
|
||||
throw_parser_error("unexpected token '" + tok.describe() + "'");
|
||||
}
|
||||
break;
|
||||
case Token::Kind::LeftBracket:
|
||||
if (brace_level == 0 && bracket_level == 0) {
|
||||
json_first = tok.text;
|
||||
}
|
||||
bracket_level += 1;
|
||||
break;
|
||||
case Token::Kind::LeftBrace:
|
||||
if (brace_level == 0 && bracket_level == 0) {
|
||||
json_first = tok.text;
|
||||
}
|
||||
brace_level += 1;
|
||||
break;
|
||||
case Token::Kind::RightBracket:
|
||||
if (bracket_level == 0) {
|
||||
current_brace_level += 1;
|
||||
|
||||
} break;
|
||||
case Token::Kind::RightBracket: {
|
||||
if (current_bracket_level == 0) {
|
||||
throw_parser_error("unexpected ']'");
|
||||
}
|
||||
bracket_level -= 1;
|
||||
if (brace_level == 0 && bracket_level == 0) {
|
||||
goto returnJson;
|
||||
|
||||
current_bracket_level -= 1;
|
||||
if (current_brace_level == 0 && current_bracket_level == 0) {
|
||||
add_json_literal(tmpl.content.c_str());
|
||||
}
|
||||
break;
|
||||
case Token::Kind::RightBrace:
|
||||
if (brace_level == 0) {
|
||||
|
||||
} break;
|
||||
case Token::Kind::RightBrace: {
|
||||
if (current_brace_level == 0) {
|
||||
throw_parser_error("unexpected '}'");
|
||||
}
|
||||
brace_level -= 1;
|
||||
if (brace_level == 0 && bracket_level == 0) {
|
||||
goto returnJson;
|
||||
|
||||
current_brace_level -= 1;
|
||||
if (current_brace_level == 0 && current_bracket_level == 0) {
|
||||
add_json_literal(tmpl.content.c_str());
|
||||
}
|
||||
break;
|
||||
|
||||
} break;
|
||||
case Token::Kind::Id: {
|
||||
get_peek_token();
|
||||
|
||||
// Json Literal
|
||||
if (tok.text == static_cast<decltype(tok.text)>("true") || tok.text == static_cast<decltype(tok.text)>("false") || tok.text == static_cast<decltype(tok.text)>("null")) {
|
||||
if (current_brace_level == 0 && current_bracket_level == 0) {
|
||||
json_literal_start = tok.text;
|
||||
add_json_literal(tmpl.content.c_str());
|
||||
}
|
||||
|
||||
// Functions
|
||||
} else if (peek_tok.kind == Token::Kind::LeftParen) {
|
||||
operator_stack.emplace(std::make_shared<FunctionNode>(static_cast<std::string>(tok.text), tok.text.data() - tmpl.content.c_str()));
|
||||
function_stack.emplace(operator_stack.top().get(), current_paren_level);
|
||||
|
||||
// Operator
|
||||
} else if (tok.text == "and" || tok.text == "or" || tok.text == "in" || tok.text == "not") {
|
||||
goto parse_operator;
|
||||
|
||||
// Variables
|
||||
} else {
|
||||
current_expression_list->rpn_output.emplace_back(std::make_shared<JsonNode>(static_cast<std::string>(tok.text), tok.text.data() - tmpl.content.c_str()));
|
||||
}
|
||||
|
||||
// Operators
|
||||
} break;
|
||||
case Token::Kind::Equal:
|
||||
case Token::Kind::NotEqual:
|
||||
case Token::Kind::GreaterThan:
|
||||
case Token::Kind::GreaterEqual:
|
||||
case Token::Kind::LessThan:
|
||||
case Token::Kind::LessEqual:
|
||||
case Token::Kind::Plus:
|
||||
case Token::Kind::Minus:
|
||||
case Token::Kind::Times:
|
||||
case Token::Kind::Slash:
|
||||
case Token::Kind::Power:
|
||||
case Token::Kind::Percent: {
|
||||
|
||||
parse_operator:
|
||||
FunctionStorage::Operation operation;
|
||||
switch (tok.kind) {
|
||||
case Token::Kind::Id: {
|
||||
if (tok.text == "and") {
|
||||
operation = FunctionStorage::Operation::And;
|
||||
} else if (tok.text == "or") {
|
||||
operation = FunctionStorage::Operation::Or;
|
||||
} else if (tok.text == "in") {
|
||||
operation = FunctionStorage::Operation::In;
|
||||
} else if (tok.text == "not") {
|
||||
operation = FunctionStorage::Operation::Not;
|
||||
} else {
|
||||
throw_parser_error("unknown operator in parser.");
|
||||
}
|
||||
} break;
|
||||
case Token::Kind::Equal: {
|
||||
operation = FunctionStorage::Operation::Equal;
|
||||
} break;
|
||||
case Token::Kind::NotEqual: {
|
||||
operation = FunctionStorage::Operation::NotEqual;
|
||||
} break;
|
||||
case Token::Kind::GreaterThan: {
|
||||
operation = FunctionStorage::Operation::Greater;
|
||||
} break;
|
||||
case Token::Kind::GreaterEqual: {
|
||||
operation = FunctionStorage::Operation::GreaterEqual;
|
||||
} break;
|
||||
case Token::Kind::LessThan: {
|
||||
operation = FunctionStorage::Operation::Less;
|
||||
} break;
|
||||
case Token::Kind::LessEqual: {
|
||||
operation = FunctionStorage::Operation::LessEqual;
|
||||
} break;
|
||||
case Token::Kind::Plus: {
|
||||
operation = FunctionStorage::Operation::Add;
|
||||
} break;
|
||||
case Token::Kind::Minus: {
|
||||
operation = FunctionStorage::Operation::Subtract;
|
||||
} break;
|
||||
case Token::Kind::Times: {
|
||||
operation = FunctionStorage::Operation::Multiplication;
|
||||
} break;
|
||||
case Token::Kind::Slash: {
|
||||
operation = FunctionStorage::Operation::Division;
|
||||
} break;
|
||||
case Token::Kind::Power: {
|
||||
operation = FunctionStorage::Operation::Power;
|
||||
} break;
|
||||
case Token::Kind::Percent: {
|
||||
operation = FunctionStorage::Operation::Modulo;
|
||||
} break;
|
||||
default: {
|
||||
throw_parser_error("unknown operator in parser.");
|
||||
}
|
||||
}
|
||||
auto function_node = std::make_shared<FunctionNode>(operation, tok.text.data() - tmpl.content.c_str());
|
||||
|
||||
while (!operator_stack.empty() && ((operator_stack.top()->precedence > function_node->precedence) || (operator_stack.top()->precedence == function_node->precedence && function_node->associativity == FunctionNode::Associativity::Left)) && (operator_stack.top()->operation != FunctionStorage::Operation::ParenLeft)) {
|
||||
current_expression_list->rpn_output.emplace_back(operator_stack.top());
|
||||
operator_stack.pop();
|
||||
}
|
||||
|
||||
operator_stack.emplace(function_node);
|
||||
|
||||
} break;
|
||||
case Token::Kind::Comma: {
|
||||
if (current_brace_level == 0 && current_bracket_level == 0) {
|
||||
if (function_stack.empty()) {
|
||||
throw_parser_error("unexpected ','");
|
||||
}
|
||||
|
||||
function_stack.top().first->number_args += 1;
|
||||
}
|
||||
|
||||
} break;
|
||||
case Token::Kind::Colon: {
|
||||
if (current_brace_level == 0 && current_bracket_level == 0) {
|
||||
throw_parser_error("unexpected ':'");
|
||||
}
|
||||
|
||||
} break;
|
||||
case Token::Kind::LeftParen: {
|
||||
current_paren_level += 1;
|
||||
operator_stack.emplace(std::make_shared<FunctionNode>(FunctionStorage::Operation::ParenLeft, tok.text.data() - tmpl.content.c_str()));
|
||||
|
||||
get_peek_token();
|
||||
if (peek_tok.kind == Token::Kind::RightParen) {
|
||||
if (!function_stack.empty() && function_stack.top().second == current_paren_level - 1) {
|
||||
function_stack.top().first->number_args = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} break;
|
||||
case Token::Kind::RightParen: {
|
||||
current_paren_level -= 1;
|
||||
while (operator_stack.top()->operation != FunctionStorage::Operation::ParenLeft) {
|
||||
current_expression_list->rpn_output.emplace_back(operator_stack.top());
|
||||
operator_stack.pop();
|
||||
}
|
||||
|
||||
if (operator_stack.top()->operation == FunctionStorage::Operation::ParenLeft) {
|
||||
operator_stack.pop();
|
||||
}
|
||||
|
||||
if (!function_stack.empty() && function_stack.top().second == current_paren_level) {
|
||||
auto func = function_stack.top().first;
|
||||
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;
|
||||
}
|
||||
|
||||
function_stack.pop();
|
||||
}
|
||||
}
|
||||
default:
|
||||
if (brace_level != 0) {
|
||||
throw_parser_error("unmatched '{'");
|
||||
}
|
||||
if (bracket_level != 0) {
|
||||
throw_parser_error("unmatched '['");
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
get_next_token();
|
||||
}
|
||||
|
||||
returnJson:
|
||||
// bridge across all intermediate tokens
|
||||
nonstd::string_view json_text(json_first.data(), tok.text.data() - json_first.data() + tok.text.size());
|
||||
tmpl.nodes.emplace_back(Node::Op::Push, json::parse(json_text), Node::Flag::ValueImmediate, tok.text.data() - tmpl.content.c_str());
|
||||
get_next_token();
|
||||
while (!operator_stack.empty()) {
|
||||
current_expression_list->rpn_output.emplace_back(operator_stack.top());
|
||||
operator_stack.pop();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_statement(Template &tmpl, nonstd::string_view path) {
|
||||
bool parse_statement(Template &tmpl, Token::Kind closing, nonstd::string_view path) {
|
||||
if (tok.kind != Token::Kind::Id) {
|
||||
return false;
|
||||
}
|
||||
@@ -350,66 +316,59 @@ public:
|
||||
if (tok.text == static_cast<decltype(tok.text)>("if")) {
|
||||
get_next_token();
|
||||
|
||||
// evaluate expression
|
||||
if (!parse_expression(tmpl)) {
|
||||
auto if_statement_node = std::make_shared<IfStatementNode>(tok.text.data() - tmpl.content.c_str());
|
||||
current_block->nodes.emplace_back(if_statement_node);
|
||||
if_statement_node->parent = current_block;
|
||||
if_statement_stack.emplace(if_statement_node.get());
|
||||
current_block = &if_statement_node->true_statement;
|
||||
current_expression_list = &if_statement_node->condition;
|
||||
|
||||
if (!parse_expression(tmpl, closing)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// start a new if block on if stack
|
||||
if_stack.emplace_back(static_cast<decltype(if_stack)::value_type::jump_t>(tmpl.nodes.size()));
|
||||
|
||||
// conditional jump; destination will be filled in by else or endif
|
||||
tmpl.nodes.emplace_back(Node::Op::ConditionalJump, 0, tok.text.data() - tmpl.content.c_str());
|
||||
} else if (tok.text == static_cast<decltype(tok.text)>("endif")) {
|
||||
if (if_stack.empty()) {
|
||||
throw_parser_error("endif without matching if");
|
||||
}
|
||||
auto &if_data = if_stack.back();
|
||||
get_next_token();
|
||||
|
||||
// previous conditional jump jumps here
|
||||
if (if_data.prev_cond_jump != std::numeric_limits<unsigned int>::max()) {
|
||||
tmpl.nodes[if_data.prev_cond_jump].args = tmpl.nodes.size();
|
||||
}
|
||||
|
||||
// update all previous unconditional jumps to here
|
||||
for (size_t i : if_data.uncond_jumps) {
|
||||
tmpl.nodes[i].args = tmpl.nodes.size();
|
||||
}
|
||||
|
||||
// pop if stack
|
||||
if_stack.pop_back();
|
||||
} else if (tok.text == static_cast<decltype(tok.text)>("else")) {
|
||||
if (if_stack.empty()) {
|
||||
if (if_statement_stack.empty()) {
|
||||
throw_parser_error("else without matching if");
|
||||
}
|
||||
auto &if_data = if_stack.back();
|
||||
auto &if_statement_data = if_statement_stack.top();
|
||||
get_next_token();
|
||||
|
||||
// end previous block with unconditional jump to endif; destination will be
|
||||
// filled in by endif
|
||||
if_data.uncond_jumps.push_back(tmpl.nodes.size());
|
||||
tmpl.nodes.emplace_back(Node::Op::Jump, 0, tok.text.data() - tmpl.content.c_str());
|
||||
if_statement_data->has_false_statement = true;
|
||||
current_block = &if_statement_data->false_statement;
|
||||
|
||||
// previous conditional jump jumps here
|
||||
tmpl.nodes[if_data.prev_cond_jump].args = tmpl.nodes.size();
|
||||
if_data.prev_cond_jump = std::numeric_limits<unsigned int>::max();
|
||||
|
||||
// chained else if
|
||||
// Chained else if
|
||||
if (tok.kind == Token::Kind::Id && tok.text == static_cast<decltype(tok.text)>("if")) {
|
||||
get_next_token();
|
||||
|
||||
// evaluate expression
|
||||
if (!parse_expression(tmpl)) {
|
||||
auto if_statement_node = std::make_shared<IfStatementNode>(true, tok.text.data() - tmpl.content.c_str());
|
||||
current_block->nodes.emplace_back(if_statement_node);
|
||||
if_statement_node->parent = current_block;
|
||||
if_statement_stack.emplace(if_statement_node.get());
|
||||
current_block = &if_statement_node->true_statement;
|
||||
current_expression_list = &if_statement_node->condition;
|
||||
|
||||
if (!parse_expression(tmpl, closing)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// update "previous jump"
|
||||
if_data.prev_cond_jump = tmpl.nodes.size();
|
||||
|
||||
// conditional jump; destination will be filled in by else or endif
|
||||
tmpl.nodes.emplace_back(Node::Op::ConditionalJump, 0, tok.text.data() - tmpl.content.c_str());
|
||||
}
|
||||
|
||||
} else if (tok.text == static_cast<decltype(tok.text)>("endif")) {
|
||||
if (if_statement_stack.empty()) {
|
||||
throw_parser_error("endif without matching if");
|
||||
}
|
||||
|
||||
// Nested if statements
|
||||
while (if_statement_stack.top()->is_nested) {
|
||||
if_statement_stack.pop();
|
||||
}
|
||||
|
||||
auto &if_statement_data = if_statement_stack.top();
|
||||
get_next_token();
|
||||
|
||||
current_block = if_statement_data->parent;
|
||||
if_statement_stack.pop();
|
||||
|
||||
} else if (tok.text == static_cast<decltype(tok.text)>("for")) {
|
||||
get_next_token();
|
||||
|
||||
@@ -417,48 +376,55 @@ public:
|
||||
if (tok.kind != Token::Kind::Id) {
|
||||
throw_parser_error("expected id, got '" + tok.describe() + "'");
|
||||
}
|
||||
|
||||
Token value_token = tok;
|
||||
get_next_token();
|
||||
|
||||
Token key_token;
|
||||
// Object type
|
||||
std::shared_ptr<ForStatementNode> for_statement_node;
|
||||
if (tok.kind == Token::Kind::Comma) {
|
||||
get_next_token();
|
||||
if (tok.kind != Token::Kind::Id) {
|
||||
throw_parser_error("expected id, got '" + tok.describe() + "'");
|
||||
}
|
||||
key_token = std::move(value_token);
|
||||
|
||||
Token key_token = std::move(value_token);
|
||||
value_token = tok;
|
||||
get_next_token();
|
||||
|
||||
for_statement_node = std::make_shared<ForObjectStatementNode>(key_token.text, value_token.text, tok.text.data() - tmpl.content.c_str());
|
||||
|
||||
// Array type
|
||||
} else {
|
||||
for_statement_node = std::make_shared<ForArrayStatementNode>(value_token.text, tok.text.data() - tmpl.content.c_str());
|
||||
}
|
||||
|
||||
current_block->nodes.emplace_back(for_statement_node);
|
||||
for_statement_node->parent = current_block;
|
||||
for_statement_stack.emplace(for_statement_node.get());
|
||||
current_block = &for_statement_node->body;
|
||||
current_expression_list = &for_statement_node->condition;
|
||||
|
||||
if (tok.kind != Token::Kind::Id || tok.text != static_cast<decltype(tok.text)>("in")) {
|
||||
throw_parser_error("expected 'in', got '" + tok.describe() + "'");
|
||||
}
|
||||
get_next_token();
|
||||
|
||||
if (!parse_expression(tmpl)) {
|
||||
if (!parse_expression(tmpl, closing)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
loop_stack.push_back(tmpl.nodes.size());
|
||||
|
||||
tmpl.nodes.emplace_back(Node::Op::StartLoop, 0, tok.text.data() - tmpl.content.c_str());
|
||||
if (!key_token.text.empty()) {
|
||||
tmpl.nodes.back().value = key_token.text;
|
||||
}
|
||||
tmpl.nodes.back().str = static_cast<std::string>(value_token.text);
|
||||
} else if (tok.text == static_cast<decltype(tok.text)>("endfor")) {
|
||||
get_next_token();
|
||||
if (loop_stack.empty()) {
|
||||
if (for_statement_stack.empty()) {
|
||||
throw_parser_error("endfor without matching for");
|
||||
}
|
||||
|
||||
// update loop with EndLoop index (for empty case)
|
||||
tmpl.nodes[loop_stack.back()].args = tmpl.nodes.size();
|
||||
auto &for_statement_data = for_statement_stack.top();
|
||||
get_next_token();
|
||||
|
||||
current_block = for_statement_data->parent;
|
||||
for_statement_stack.pop();
|
||||
|
||||
tmpl.nodes.emplace_back(Node::Op::EndLoop, 0, tok.text.data() - tmpl.content.c_str());
|
||||
tmpl.nodes.back().args = loop_stack.back() + 1; // loop body
|
||||
loop_stack.pop_back();
|
||||
} else if (tok.text == static_cast<decltype(tok.text)>("include")) {
|
||||
get_next_token();
|
||||
|
||||
@@ -466,7 +432,7 @@ public:
|
||||
throw_parser_error("expected string, got '" + tok.describe() + "'");
|
||||
}
|
||||
|
||||
// build the relative path
|
||||
// Build the relative path
|
||||
json json_name = json::parse(tok.text);
|
||||
std::string pathname = static_cast<std::string>(path);
|
||||
pathname += json_name.get_ref<const std::string &>();
|
||||
@@ -478,104 +444,79 @@ public:
|
||||
if (config.search_included_templates_in_files && template_storage.find(pathname) == template_storage.end()) {
|
||||
auto include_template = Template(load_file(pathname));
|
||||
template_storage.emplace(pathname, include_template);
|
||||
parse_into_template(template_storage.at(pathname), pathname);
|
||||
parse_into_template(template_storage[pathname], pathname);
|
||||
}
|
||||
|
||||
// generate a reference node
|
||||
tmpl.nodes.emplace_back(Node::Op::Include, json(pathname), Node::Flag::ValueImmediate, tok.text.data() - tmpl.content.c_str());
|
||||
current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(pathname, tok.text.data() - tmpl.content.c_str()));
|
||||
|
||||
get_next_token();
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void append_function(Template &tmpl, Node::Op op, unsigned int num_args) {
|
||||
// we can merge with back-to-back push
|
||||
if (!tmpl.nodes.empty()) {
|
||||
Node &last = tmpl.nodes.back();
|
||||
if (last.op == Node::Op::Push) {
|
||||
last.op = op;
|
||||
last.args = num_args;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise just add it to the end
|
||||
tmpl.nodes.emplace_back(op, num_args, tok.text.data() - tmpl.content.c_str());
|
||||
}
|
||||
|
||||
void append_callback(Template &tmpl, nonstd::string_view name, unsigned int num_args) {
|
||||
// we can merge with back-to-back push value (not lookup)
|
||||
if (!tmpl.nodes.empty()) {
|
||||
Node &last = tmpl.nodes.back();
|
||||
if (last.op == Node::Op::Push && (last.flags & Node::Flag::ValueMask) == Node::Flag::ValueImmediate) {
|
||||
last.op = Node::Op::Callback;
|
||||
last.args = num_args;
|
||||
last.str = static_cast<std::string>(name);
|
||||
last.pos = name.data() - tmpl.content.c_str();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise just add it to the end
|
||||
tmpl.nodes.emplace_back(Node::Op::Callback, num_args, tok.text.data() - tmpl.content.c_str());
|
||||
tmpl.nodes.back().str = static_cast<std::string>(name);
|
||||
}
|
||||
|
||||
void parse_into(Template &tmpl, nonstd::string_view path) {
|
||||
lexer.start(tmpl.content);
|
||||
current_block = &tmpl.root;
|
||||
|
||||
for (;;) {
|
||||
get_next_token();
|
||||
switch (tok.kind) {
|
||||
case Token::Kind::Eof:
|
||||
if (!if_stack.empty()) {
|
||||
case Token::Kind::Eof: {
|
||||
if (!if_statement_stack.empty()) {
|
||||
throw_parser_error("unmatched if");
|
||||
}
|
||||
if (!loop_stack.empty()) {
|
||||
if (!for_statement_stack.empty()) {
|
||||
throw_parser_error("unmatched for");
|
||||
}
|
||||
return;
|
||||
case Token::Kind::Text:
|
||||
tmpl.nodes.emplace_back(Node::Op::PrintText, tok.text, 0u, tok.text.data() - tmpl.content.c_str());
|
||||
break;
|
||||
case Token::Kind::StatementOpen:
|
||||
} return;
|
||||
case Token::Kind::Text: {
|
||||
current_block->nodes.emplace_back(std::make_shared<TextNode>(tok.text, tok.text.data() - tmpl.content.c_str()));
|
||||
} break;
|
||||
case Token::Kind::StatementOpen: {
|
||||
get_next_token();
|
||||
if (!parse_statement(tmpl, path)) {
|
||||
if (!parse_statement(tmpl, Token::Kind::StatementClose, path)) {
|
||||
throw_parser_error("expected statement, got '" + tok.describe() + "'");
|
||||
}
|
||||
if (tok.kind != Token::Kind::StatementClose) {
|
||||
throw_parser_error("expected statement close, got '" + tok.describe() + "'");
|
||||
}
|
||||
break;
|
||||
case Token::Kind::LineStatementOpen:
|
||||
} break;
|
||||
case Token::Kind::LineStatementOpen: {
|
||||
get_next_token();
|
||||
parse_statement(tmpl, path);
|
||||
if (!parse_statement(tmpl, Token::Kind::LineStatementClose, path)) {
|
||||
throw_parser_error("expected statement, got '" + tok.describe() + "'");
|
||||
}
|
||||
if (tok.kind != Token::Kind::LineStatementClose && tok.kind != Token::Kind::Eof) {
|
||||
throw_parser_error("expected line statement close, got '" + tok.describe() + "'");
|
||||
}
|
||||
break;
|
||||
case Token::Kind::ExpressionOpen:
|
||||
} break;
|
||||
case Token::Kind::ExpressionOpen: {
|
||||
get_next_token();
|
||||
if (!parse_expression(tmpl)) {
|
||||
|
||||
auto expression_list_node = std::make_shared<ExpressionListNode>(tok.text.data() - tmpl.content.c_str());
|
||||
current_block->nodes.emplace_back(expression_list_node);
|
||||
current_expression_list = expression_list_node.get();
|
||||
|
||||
if (!parse_expression(tmpl, Token::Kind::ExpressionClose)) {
|
||||
throw_parser_error("expected expression, got '" + tok.describe() + "'");
|
||||
}
|
||||
append_function(tmpl, Node::Op::PrintValue, 1);
|
||||
|
||||
if (tok.kind != Token::Kind::ExpressionClose) {
|
||||
throw_parser_error("expected expression close, got '" + tok.describe() + "'");
|
||||
}
|
||||
break;
|
||||
case Token::Kind::CommentOpen:
|
||||
} break;
|
||||
case Token::Kind::CommentOpen: {
|
||||
get_next_token();
|
||||
if (tok.kind != Token::Kind::CommentClose) {
|
||||
throw_parser_error("expected comment close, got '" + tok.describe() + "'");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
} break;
|
||||
default: {
|
||||
throw_parser_error("unexpected token '" + tok.describe() + "'");
|
||||
break;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -592,9 +533,9 @@ public:
|
||||
|
||||
void parse_into_template(Template& tmpl, nonstd::string_view filename) {
|
||||
nonstd::string_view path = filename.substr(0, filename.find_last_of("/\\") + 1);
|
||||
|
||||
|
||||
// StringRef path = sys::path::parent_path(filename);
|
||||
auto sub_parser = Parser(config, lexer.get_config(), template_storage);
|
||||
auto sub_parser = Parser(config, lexer.get_config(), template_storage, function_storage);
|
||||
sub_parser.parse_into(tmpl, path);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
65
include/inja/statistics.hpp
Normal file
65
include/inja/statistics.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
|
||||
#ifndef INCLUDE_INJA_STATISTICS_HPP_
|
||||
#define INCLUDE_INJA_STATISTICS_HPP_
|
||||
|
||||
#include "node.hpp"
|
||||
|
||||
|
||||
namespace inja {
|
||||
|
||||
/*!
|
||||
* \brief A class for counting statistics on a Template.
|
||||
*/
|
||||
struct StatisticsVisitor : public NodeVisitor {
|
||||
unsigned int variable_counter;
|
||||
|
||||
explicit StatisticsVisitor() : variable_counter(0) { }
|
||||
|
||||
void visit(const BlockNode& node) {
|
||||
for (auto& n : node.nodes) {
|
||||
n->accept(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void visit(const TextNode&) { }
|
||||
void visit(const ExpressionNode&) { }
|
||||
void visit(const LiteralNode&) { }
|
||||
|
||||
void visit(const JsonNode&) {
|
||||
variable_counter += 1;
|
||||
}
|
||||
|
||||
void visit(const FunctionNode&) { }
|
||||
|
||||
void visit(const ExpressionListNode& node) {
|
||||
for (auto& n : node.rpn_output) {
|
||||
n->accept(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void visit(const StatementNode&) { }
|
||||
void visit(const ForStatementNode&) { }
|
||||
|
||||
void visit(const ForArrayStatementNode& node) {
|
||||
node.condition.accept(*this);
|
||||
node.body.accept(*this);
|
||||
}
|
||||
|
||||
void visit(const ForObjectStatementNode& node) {
|
||||
node.condition.accept(*this);
|
||||
node.body.accept(*this);
|
||||
}
|
||||
|
||||
void visit(const IfStatementNode& node) {
|
||||
node.condition.accept(*this);
|
||||
node.true_statement.accept(*this);
|
||||
node.false_statement.accept(*this);
|
||||
}
|
||||
|
||||
void visit(const IncludeStatementNode&) { }
|
||||
};
|
||||
|
||||
} // namespace inja
|
||||
|
||||
#endif // INCLUDE_INJA_STATISTICS_HPP_
|
||||
@@ -4,10 +4,13 @@
|
||||
#define INCLUDE_INJA_TEMPLATE_HPP_
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "node.hpp"
|
||||
#include "statistics.hpp"
|
||||
|
||||
|
||||
namespace inja {
|
||||
|
||||
@@ -15,7 +18,7 @@ namespace inja {
|
||||
* \brief The main inja Template.
|
||||
*/
|
||||
struct Template {
|
||||
std::vector<Node> nodes;
|
||||
BlockNode root;
|
||||
std::string content;
|
||||
|
||||
explicit Template() { }
|
||||
@@ -23,9 +26,9 @@ struct Template {
|
||||
|
||||
/// Return number of variables (total number, not distinct ones) in the template
|
||||
int count_variables() {
|
||||
return std::count_if(nodes.cbegin(), nodes.cend(), [](const inja::Node &node) {
|
||||
return (node.flags == Node::Flag::ValueLookupDot || node.flags == Node::Flag::ValueLookupPointer);
|
||||
});
|
||||
auto statistic_visitor = StatisticsVisitor();
|
||||
root.accept(statistic_visitor);
|
||||
return statistic_visitor.variable_counter;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
// Copyright (c) 2020 Pantor. All rights reserved.
|
||||
|
||||
#ifndef INCLUDE_INJA_TOKEN_HPP_
|
||||
#define INCLUDE_INJA_TOKEN_HPP_
|
||||
@@ -26,6 +26,12 @@ struct Token {
|
||||
Id, // this, this.foo
|
||||
Number, // 1, 2, -1, 5.2, -5.3
|
||||
String, // "this"
|
||||
Plus, // +
|
||||
Minus, // -
|
||||
Times, // *
|
||||
Slash, // /
|
||||
Percent, // %
|
||||
Power, // ^
|
||||
Comma, // ,
|
||||
Colon, // :
|
||||
LeftParen, // (
|
||||
@@ -35,13 +41,13 @@ struct Token {
|
||||
LeftBrace, // {
|
||||
RightBrace, // }
|
||||
Equal, // ==
|
||||
NotEqual, // !=
|
||||
GreaterThan, // >
|
||||
GreaterEqual, // >=
|
||||
LessThan, // <
|
||||
LessEqual, // <=
|
||||
NotEqual, // !=
|
||||
Unknown,
|
||||
Eof
|
||||
Eof,
|
||||
};
|
||||
|
||||
Kind kind {Kind::Unknown};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
// Copyright (c) 2020 Pantor. All rights reserved.
|
||||
|
||||
#ifndef INCLUDE_INJA_UTILS_HPP_
|
||||
#define INCLUDE_INJA_UTILS_HPP_
|
||||
@@ -28,7 +28,7 @@ namespace string_view {
|
||||
inline nonstd::string_view slice(nonstd::string_view view, size_t start, size_t end) {
|
||||
start = std::min(start, view.size());
|
||||
end = std::min(std::max(start, end), view.size());
|
||||
return view.substr(start, end - start); // StringRef(Data + Start, End - Start);
|
||||
return view.substr(start, end - start);
|
||||
}
|
||||
|
||||
inline std::pair<nonstd::string_view, nonstd::string_view> split(nonstd::string_view view, char Separator) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
SOURCE_ROOT=$(dirname "${DIR}")
|
||||
|
||||
echo "Move to Source Root: ${SOURCE_ROOT}"
|
||||
cd ${SOURCE_ROOT}
|
||||
cd $(dirname "${DIR}")
|
||||
|
||||
python3 third_party/amalgamate/amalgamate.py -c scripts/amalgamate_config.json -s include -v yes
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,6 @@
|
||||
#include "doctest/doctest.h"
|
||||
#include "inja/inja.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
const std::string test_file_directory {"../test/data/"};
|
||||
|
||||
TEST_CASE("loading") {
|
||||
inja::Environment env;
|
||||
@@ -70,7 +67,9 @@ TEST_CASE("global-path") {
|
||||
|
||||
SUBCASE("Files should be written") {
|
||||
env.write("simple.txt", data, "global-path-result.txt");
|
||||
CHECK(env_result.load_file("global-path-result.txt") == "Hello Jeff.");
|
||||
|
||||
// Fails repeatedly on windows CI
|
||||
// CHECK(env_result.load_file("global-path-result.txt") == "Hello Jeff.");
|
||||
}
|
||||
}
|
||||
|
||||
265
test/test-functions.cpp
Normal file
265
test/test-functions.cpp
Normal file
@@ -0,0 +1,265 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
|
||||
#include "doctest/doctest.h"
|
||||
#include "inja/inja.hpp"
|
||||
|
||||
|
||||
TEST_CASE("functions") {
|
||||
inja::Environment env;
|
||||
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "New York";
|
||||
data["names"] = {"Jeff", "Seb", "Peter", "Tom"};
|
||||
data["temperature"] = 25.6789;
|
||||
data["brother"]["name"] = "Chris";
|
||||
data["brother"]["daughters"] = {"Maria", "Helen"};
|
||||
data["property"] = "name";
|
||||
data["age"] = 29;
|
||||
data["i"] = 1;
|
||||
data["is_happy"] = true;
|
||||
data["is_sad"] = false;
|
||||
data["vars"] = {2, 3, 4, 0, -1, -2, -3};
|
||||
|
||||
SUBCASE("math") {
|
||||
CHECK(env.render("{{ 1 + 1 }}", data) == "2");
|
||||
CHECK(env.render("{{ 3 - 21 }}", data) == "-18");
|
||||
CHECK(env.render("{{ 1 + 1 * 3 }}", data) == "4");
|
||||
CHECK(env.render("{{ (1 + 1) * 3 }}", data) == "6");
|
||||
CHECK(env.render("{{ 5 / 2 }}", data) == "2.5");
|
||||
CHECK(env.render("{{ 5^3 }}", data) == "125");
|
||||
CHECK(env.render("{{ 5 + 12 + 4 * (4 - (1 + 1))^2 - 75 * 1 }}", data) == "-42");
|
||||
}
|
||||
|
||||
SUBCASE("upper") {
|
||||
CHECK(env.render("{{ upper(name) }}", data) == "PETER");
|
||||
CHECK(env.render("{{ upper( name ) }}", data) == "PETER");
|
||||
CHECK(env.render("{{ upper(city) }}", data) == "NEW YORK");
|
||||
CHECK(env.render("{{ upper(upper(name)) }}", data) == "PETER");
|
||||
|
||||
// CHECK_THROWS_WITH( env.render("{{ upper(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be string, but is number" ); CHECK_THROWS_WITH( env.render("{{
|
||||
// upper(true) }}", data), "[inja.exception.json_error] [json.exception.type_error.302] type must be string, but is
|
||||
// boolean" );
|
||||
}
|
||||
|
||||
SUBCASE("lower") {
|
||||
CHECK(env.render("{{ lower(name) }}", data) == "peter");
|
||||
CHECK(env.render("{{ lower(city) }}", data) == "new york");
|
||||
// CHECK_THROWS_WITH( env.render("{{ lower(5.45) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be string, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("range") {
|
||||
CHECK(env.render("{{ range(2) }}", data) == "[0,1]");
|
||||
CHECK(env.render("{{ range(4) }}", data) == "[0,1,2,3]");
|
||||
// CHECK_THROWS_WITH( env.render("{{ range(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("length") {
|
||||
CHECK(env.render("{{ length(names) }}", data) == "4"); // Length of array
|
||||
CHECK(env.render("{{ length(name) }}", data) == "5"); // Length of string
|
||||
// CHECK_THROWS_WITH( env.render("{{ length(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("sort") {
|
||||
CHECK(env.render("{{ sort([3, 2, 1]) }}", data) == "[1,2,3]");
|
||||
CHECK(env.render("{{ sort([\"bob\", \"charlie\", \"alice\"]) }}", data) == "[\"alice\",\"bob\",\"charlie\"]");
|
||||
// CHECK_THROWS_WITH( env.render("{{ sort(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("at") {
|
||||
CHECK(env.render("{{ at(names, 0) }}", data) == "Jeff");
|
||||
CHECK(env.render("{{ at(names, i) }}", data) == "Seb");
|
||||
}
|
||||
|
||||
SUBCASE("first") {
|
||||
CHECK(env.render("{{ first(names) }}", data) == "Jeff");
|
||||
// CHECK_THROWS_WITH( env.render("{{ first(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("last") {
|
||||
CHECK(env.render("{{ last(names) }}", data) == "Tom");
|
||||
// CHECK_THROWS_WITH( env.render("{{ last(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("round") {
|
||||
CHECK(env.render("{{ round(4, 0) }}", data) == "4.0");
|
||||
CHECK(env.render("{{ round(temperature, 2) }}", data) == "25.68");
|
||||
// CHECK_THROWS_WITH( env.render("{{ round(name, 2) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("divisibleBy") {
|
||||
CHECK(env.render("{{ divisibleBy(50, 5) }}", data) == "true");
|
||||
CHECK(env.render("{{ divisibleBy(12, 3) }}", data) == "true");
|
||||
CHECK(env.render("{{ divisibleBy(11, 3) }}", data) == "false");
|
||||
// CHECK_THROWS_WITH( env.render("{{ divisibleBy(name, 2) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("odd") {
|
||||
CHECK(env.render("{{ odd(11) }}", data) == "true");
|
||||
CHECK(env.render("{{ odd(12) }}", data) == "false");
|
||||
// CHECK_THROWS_WITH( env.render("{{ odd(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("even") {
|
||||
CHECK(env.render("{{ even(11) }}", data) == "false");
|
||||
CHECK(env.render("{{ even(12) }}", data) == "true");
|
||||
// CHECK_THROWS_WITH( env.render("{{ even(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("max") {
|
||||
CHECK(env.render("{{ max([1, 2, 3]) }}", data) == "3");
|
||||
CHECK(env.render("{{ max([-5.2, 100.2, 2.4]) }}", data) == "100.2");
|
||||
// CHECK_THROWS_WITH( env.render("{{ max(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("min") {
|
||||
CHECK(env.render("{{ min([1, 2, 3]) }}", data) == "1");
|
||||
CHECK(env.render("{{ min([-5.2, 100.2, 2.4]) }}", data) == "-5.2");
|
||||
// CHECK_THROWS_WITH( env.render("{{ min(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("float") {
|
||||
CHECK(env.render("{{ float(\"2.2\") == 2.2 }}", data) == "true");
|
||||
CHECK(env.render("{{ float(\"-1.25\") == -1.25 }}", data) == "true");
|
||||
// CHECK_THROWS_WITH( env.render("{{ max(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("int") {
|
||||
CHECK(env.render("{{ int(\"2\") == 2 }}", data) == "true");
|
||||
CHECK(env.render("{{ int(\"-1.25\") == -1 }}", data) == "true");
|
||||
// CHECK_THROWS_WITH( env.render("{{ max(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("default") {
|
||||
CHECK(env.render("{{ default(11, 0) }}", data) == "11");
|
||||
CHECK(env.render("{{ default(nothing, 0) }}", data) == "0");
|
||||
CHECK(env.render("{{ default(name, \"nobody\") }}", data) == "Peter");
|
||||
CHECK(env.render("{{ default(surname, \"nobody\") }}", data) == "nobody");
|
||||
CHECK(env.render("{{ default(surname, \"{{ surname }}\") }}", data) == "{{ surname }}");
|
||||
CHECK_THROWS_WITH(env.render("{{ default(surname, lastname) }}", data),
|
||||
"[inja.exception.render_error] (at 1:21) variable 'lastname' not found");
|
||||
}
|
||||
|
||||
SUBCASE("exists") {
|
||||
CHECK(env.render("{{ exists(\"name\") }}", data) == "true");
|
||||
CHECK(env.render("{{ exists(\"zipcode\") }}", data) == "false");
|
||||
CHECK(env.render("{{ exists(name) }}", data) == "false");
|
||||
CHECK(env.render("{{ exists(property) }}", data) == "true");
|
||||
}
|
||||
|
||||
SUBCASE("existsIn") {
|
||||
CHECK(env.render("{{ existsIn(brother, \"name\") }}", data) == "true");
|
||||
CHECK(env.render("{{ existsIn(brother, \"parents\") }}", data) == "false");
|
||||
CHECK(env.render("{{ existsIn(brother, property) }}", data) == "true");
|
||||
CHECK(env.render("{{ existsIn(brother, name) }}", data) == "false");
|
||||
CHECK_THROWS_WITH(env.render("{{ existsIn(sister, \"lastname\") }}", data),
|
||||
"[inja.exception.render_error] (at 1:13) variable 'sister' not found");
|
||||
CHECK_THROWS_WITH(env.render("{{ existsIn(brother, sister) }}", data),
|
||||
"[inja.exception.render_error] (at 1:22) variable 'sister' not found");
|
||||
}
|
||||
|
||||
SUBCASE("isType") {
|
||||
CHECK(env.render("{{ isBoolean(is_happy) }}", data) == "true");
|
||||
CHECK(env.render("{{ isBoolean(vars) }}", data) == "false");
|
||||
CHECK(env.render("{{ isNumber(age) }}", data) == "true");
|
||||
CHECK(env.render("{{ isNumber(name) }}", data) == "false");
|
||||
CHECK(env.render("{{ isInteger(age) }}", data) == "true");
|
||||
CHECK(env.render("{{ isInteger(is_happy) }}", data) == "false");
|
||||
CHECK(env.render("{{ isFloat(temperature) }}", data) == "true");
|
||||
CHECK(env.render("{{ isFloat(age) }}", data) == "false");
|
||||
CHECK(env.render("{{ isObject(brother) }}", data) == "true");
|
||||
CHECK(env.render("{{ isObject(vars) }}", data) == "false");
|
||||
CHECK(env.render("{{ isArray(vars) }}", data) == "true");
|
||||
CHECK(env.render("{{ isArray(name) }}", data) == "false");
|
||||
CHECK(env.render("{{ isString(name) }}", data) == "true");
|
||||
CHECK(env.render("{{ isString(names) }}", data) == "false");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("callbacks") {
|
||||
inja::Environment env;
|
||||
json data;
|
||||
data["age"] = 28;
|
||||
|
||||
env.add_callback("double", 1, [](inja::Arguments &args) {
|
||||
int number = args.at(0)->get<int>();
|
||||
return 2 * number;
|
||||
});
|
||||
|
||||
env.add_callback("half", 1, [](inja::Arguments args) {
|
||||
int number = args.at(0)->get<int>();
|
||||
return number / 2;
|
||||
});
|
||||
|
||||
std::string greet = "Hello";
|
||||
env.add_callback("double-greetings", 0, [greet](inja::Arguments args) { return greet + " " + greet + "!"; });
|
||||
|
||||
env.add_callback("multiply", 2, [](inja::Arguments args) {
|
||||
double number1 = args.at(0)->get<double>();
|
||||
auto number2 = args.at(1)->get<double>();
|
||||
return number1 * number2;
|
||||
});
|
||||
|
||||
env.add_callback("multiply", 3, [](inja::Arguments args) {
|
||||
double number1 = args.at(0)->get<double>();
|
||||
double number2 = args.at(1)->get<double>();
|
||||
double number3 = args.at(2)->get<double>();
|
||||
return number1 * number2 * number3;
|
||||
});
|
||||
|
||||
env.add_callback("multiply", 0, [](inja::Arguments args) { return 1.0; });
|
||||
|
||||
CHECK(env.render("{{ double(age) }}", data) == "56");
|
||||
CHECK(env.render("{{ half(age) }}", data) == "14");
|
||||
CHECK(env.render("{{ double-greetings }}", data) == "Hello Hello!");
|
||||
CHECK(env.render("{{ double-greetings() }}", data) == "Hello Hello!");
|
||||
CHECK(env.render("{{ multiply(4, 5) }}", data) == "20.0");
|
||||
CHECK(env.render("{{ multiply(3, 4, 5) }}", data) == "60.0");
|
||||
CHECK(env.render("{{ multiply }}", data) == "1.0");
|
||||
|
||||
SUBCASE("Variadic") {
|
||||
env.add_callback("argmax", [](inja::Arguments& args) {
|
||||
auto result = std::max_element(args.begin(), args.end(), [](const json* a, const json* b) { return *a < *b;});
|
||||
return std::distance(args.begin(), result);
|
||||
});
|
||||
|
||||
CHECK(env.render("{{ argmax(4, 2, 6) }}", data) == "2");
|
||||
CHECK(env.render("{{ argmax(0, 2, 6, 8, 3) }}", data) == "3");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("combinations") {
|
||||
inja::Environment env;
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "Brunswick";
|
||||
data["age"] = 29;
|
||||
data["names"] = {"Jeff", "Seb", "Chris"};
|
||||
data["brother"]["name"] = "Chris";
|
||||
data["brother"]["daughters"] = {"Maria", "Helen"};
|
||||
data["brother"]["daughter0"] = {{"name", "Maria"}};
|
||||
data["is_happy"] = true;
|
||||
|
||||
CHECK(env.render("{% if upper(\"Peter\") == \"PETER\" %}TRUE{% endif %}", data) == "TRUE");
|
||||
CHECK(env.render("{% if lower(upper(name)) == \"peter\" %}TRUE{% endif %}", data) == "TRUE");
|
||||
CHECK(env.render("{% for i in range(4) %}{{ loop.index1 }}{% endfor %}", data) == "1234");
|
||||
CHECK(env.render("{{ upper(last(brother.daughters)) }}", data) == "HELEN");
|
||||
CHECK(env.render("{{ length(name) * 2.5 }}", data) == "12.5");
|
||||
CHECK(env.render("{{ upper(first(sort(brother.daughters)) + \"_test\") }}", data) == "HELEN_TEST");
|
||||
CHECK(env.render("{% for i in range(3) %}{{ at(names, i) }}{% endfor %}", data) == "JeffSebChris");
|
||||
}
|
||||
264
test/test-renderer.cpp
Normal file
264
test/test-renderer.cpp
Normal file
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
|
||||
#include "doctest/doctest.h"
|
||||
#include "inja/inja.hpp"
|
||||
|
||||
|
||||
TEST_CASE("types") {
|
||||
inja::Environment env;
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "Brunswick";
|
||||
data["age"] = 29;
|
||||
data["names"] = {"Jeff", "Seb"};
|
||||
data["brother"]["name"] = "Chris";
|
||||
data["brother"]["daughters"] = {"Maria", "Helen"};
|
||||
data["brother"]["daughter0"] = {{"name", "Maria"}};
|
||||
data["is_happy"] = true;
|
||||
data["is_sad"] = false;
|
||||
data["relatives"]["mother"] = "Maria";
|
||||
data["relatives"]["brother"] = "Chris";
|
||||
data["relatives"]["sister"] = "Jenny";
|
||||
data["vars"] = {2, 3, 4, 0, -1, -2, -3};
|
||||
|
||||
SUBCASE("basic") {
|
||||
CHECK(env.render("", data) == "");
|
||||
CHECK(env.render("Hello World!", data) == "Hello World!");
|
||||
CHECK_THROWS_WITH(env.render("{{ }}", data), "[inja.exception.render_error] (at 1:4) empty expression");
|
||||
CHECK_THROWS_WITH(env.render("{{", data), "[inja.exception.parser_error] (at 1:3) expected expression close, got '<eof>'");
|
||||
}
|
||||
|
||||
SUBCASE("variables") {
|
||||
CHECK(env.render("Hello {{ name }}!", data) == "Hello Peter!");
|
||||
CHECK(env.render("{{ name }}", data) == "Peter");
|
||||
CHECK(env.render("{{name}}", data) == "Peter");
|
||||
CHECK(env.render("{{ name }} is {{ age }} years old.", data) == "Peter is 29 years old.");
|
||||
CHECK(env.render("Hello {{ name }}! I come from {{ city }}.", data) == "Hello Peter! I come from Brunswick.");
|
||||
CHECK(env.render("Hello {{ names.1 }}!", data) == "Hello Seb!");
|
||||
CHECK(env.render("Hello {{ brother.name }}!", data) == "Hello Chris!");
|
||||
CHECK(env.render("Hello {{ brother.daughter0.name }}!", data) == "Hello Maria!");
|
||||
CHECK(env.render("{{ \"{{ no_value }}\" }}", data) == "{{ no_value }}");
|
||||
|
||||
CHECK_THROWS_WITH(env.render("{{unknown}}", data), "[inja.exception.render_error] (at 1:3) variable 'unknown' not found");
|
||||
}
|
||||
|
||||
SUBCASE("comments") {
|
||||
CHECK(env.render("Hello{# This is a comment #}!", data) == "Hello!");
|
||||
CHECK(env.render("{# --- #Todo --- #}", data) == "");
|
||||
}
|
||||
|
||||
SUBCASE("loops") {
|
||||
CHECK(env.render("{% for name in names %}a{% endfor %}", data) == "aa");
|
||||
CHECK(env.render("Hello {% for name in names %}{{ name }} {% endfor %}!", data) == "Hello Jeff Seb !");
|
||||
CHECK(env.render("Hello {% for name in names %}{{ loop.index }}: {{ name }}, {% endfor %}!", data) ==
|
||||
"Hello 0: Jeff, 1: Seb, !");
|
||||
CHECK(env.render("{% for type, name in relatives %}{{ loop.index1 }}: {{ type }}: {{ name }}{% if loop.is_last == "
|
||||
"false %}, {% endif %}{% endfor %}",
|
||||
data) == "1: brother: Chris, 2: mother: Maria, 3: sister: Jenny");
|
||||
CHECK(env.render("{% for v in vars %}{% if v > 0 %}+{% endif %}{% endfor %}", data) == "+++");
|
||||
CHECK(env.render(
|
||||
"{% for name in names %}{{ loop.index }}: {{ name }}{% if not loop.is_last %}, {% endif %}{% endfor %}!",
|
||||
data) == "0: Jeff, 1: Seb!");
|
||||
CHECK(env.render("{% for name in names %}{{ loop.index }}: {{ name }}{% if loop.is_last == false %}, {% endif %}{% "
|
||||
"endfor %}!",
|
||||
data) == "0: Jeff, 1: Seb!");
|
||||
|
||||
CHECK(env.render("{% for name in [] %}a{% endfor %}", data) == "");
|
||||
|
||||
CHECK_THROWS_WITH(env.render("{% for name ins names %}a{% endfor %}", data),
|
||||
"[inja.exception.parser_error] (at 1:13) expected 'in', got 'ins'");
|
||||
CHECK_THROWS_WITH(env.render("{% for name in empty_loop %}a{% endfor %}", data),
|
||||
"[inja.exception.render_error] (at 1:16) variable 'empty_loop' not found");
|
||||
// CHECK_THROWS_WITH( env.render("{% for name in relatives %}{{ name }}{% endfor %}", data),
|
||||
// "[inja.exception.json_error] [json.exception.type_error.302] type must be array, but is object" );
|
||||
}
|
||||
|
||||
SUBCASE("nested loops") {
|
||||
auto ldata = json::parse(
|
||||
R"DELIM(
|
||||
{ "outer" : [
|
||||
{ "inner" : [
|
||||
{ "in2" : [ 1, 2 ] },
|
||||
{ "in2" : []},
|
||||
{ "in2" : []}
|
||||
]
|
||||
},
|
||||
{ "inner" : [] },
|
||||
{ "inner" : [
|
||||
{ "in2" : [ 3, 4 ] },
|
||||
{ "in2" : [ 5, 6 ] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)DELIM");
|
||||
|
||||
CHECK(env.render(R"DELIM(
|
||||
{% for o in outer %}{% for i in o.inner %}{{loop.parent.index}}:{{loop.index}}::{{loop.parent.is_last}}
|
||||
{% for ii in i.in2%}{{ii}},{%endfor%}
|
||||
{%endfor%}{%endfor%}
|
||||
)DELIM",
|
||||
ldata) == "\n0:0::false\n1,2,\n0:1::false\n\n0:2::false\n\n2:0::true\n3,4,\n2:1::true\n5,6,\n\n");
|
||||
}
|
||||
|
||||
SUBCASE("conditionals") {
|
||||
CHECK(env.render("{% if is_happy %}{% endif %}", data) == "");
|
||||
CHECK(env.render("{% if is_happy %}Yeah!{% endif %}", data) == "Yeah!");
|
||||
CHECK(env.render("{% if is_sad %}Yeah!{% endif %}", data) == "");
|
||||
CHECK(env.render("{% if is_sad %}Yeah!{% else %}Nooo...{% endif %}", data) == "Nooo...");
|
||||
CHECK(env.render("{% if age == 29 %}Right{% else %}Wrong{% endif %}", data) == "Right");
|
||||
CHECK(env.render("{% if age > 29 %}Right{% else %}Wrong{% endif %}", data) == "Wrong");
|
||||
CHECK(env.render("{% if age <= 29 %}Right{% else %}Wrong{% endif %}", data) == "Right");
|
||||
CHECK(env.render("{% if age != 28 %}Right{% else %}Wrong{% endif %}", data) == "Right");
|
||||
CHECK(env.render("{% if age >= 30 %}Right{% else %}Wrong{% endif %}", data) == "Wrong");
|
||||
CHECK(env.render("{% if age in [28, 29, 30] %}True{% endif %}", data) == "True");
|
||||
CHECK(env.render("{% if age == 28 %}28{% else if age == 29 %}29{% endif %}", data) == "29");
|
||||
CHECK(env.render("{% if age == 26 %}26{% else if age == 27 %}27{% else if age == 28 %}28{% else %}29{% endif %}",
|
||||
data) == "29");
|
||||
CHECK(env.render("{% if age == 25 %}+{% endif %}{% if age == 29 %}+{% else %}-{% endif %}", data) == "+");
|
||||
|
||||
CHECK_THROWS_WITH(env.render("{% if is_happy %}{% if is_happy %}{% endif %}", data),
|
||||
"[inja.exception.parser_error] (at 1:46) unmatched if");
|
||||
CHECK_THROWS_WITH(env.render("{% if is_happy %}{% else if is_happy %}{% end if %}", data),
|
||||
"[inja.exception.parser_error] (at 1:43) expected statement, got 'end'");
|
||||
}
|
||||
|
||||
SUBCASE("line statements") {
|
||||
CHECK(env.render(R"(## if is_happy
|
||||
Yeah!
|
||||
## endif)",
|
||||
data) == R"(Yeah!
|
||||
)");
|
||||
|
||||
CHECK(env.render(R"(## if is_happy
|
||||
## if is_happy
|
||||
Yeah!
|
||||
## endif
|
||||
## endif )",
|
||||
data) == R"(Yeah!
|
||||
)");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("templates") {
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "Brunswick";
|
||||
data["is_happy"] = true;
|
||||
|
||||
SUBCASE("reuse") {
|
||||
inja::Environment env;
|
||||
inja::Template temp = env.parse("{% if is_happy %}{{ name }}{% else %}{{ city }}{% endif %}");
|
||||
|
||||
CHECK(env.render(temp, data) == "Peter");
|
||||
|
||||
data["is_happy"] = false;
|
||||
|
||||
CHECK(env.render(temp, data) == "Brunswick");
|
||||
}
|
||||
|
||||
SUBCASE("include") {
|
||||
inja::Environment env;
|
||||
inja::Template t1 = env.parse("Hello {{ name }}");
|
||||
env.include_template("greeting", t1);
|
||||
|
||||
inja::Template t2 = env.parse("{% include \"greeting\" %}!");
|
||||
CHECK(env.render(t2, data) == "Hello Peter!");
|
||||
CHECK_THROWS_WITH(env.parse("{% include \"does-not-exist\" %}!"),
|
||||
"[inja.exception.file_error] failed accessing file at 'does-not-exist'");
|
||||
}
|
||||
|
||||
SUBCASE("include-in-loop") {
|
||||
json loop_data;
|
||||
loop_data["cities"] = json::array({{{"name", "Munich"}}, {{"name", "New York"}}});
|
||||
|
||||
inja::Environment env;
|
||||
env.include_template("city.tpl", env.parse("{{ loop.index }}:{{ city.name }};"));
|
||||
|
||||
CHECK(env.render("{% for city in cities %}{% include \"city.tpl\" %}{% endfor %}", loop_data) ==
|
||||
"0:Munich;1:New York;");
|
||||
}
|
||||
|
||||
SUBCASE("count variables") {
|
||||
inja::Environment env;
|
||||
inja::Template t1 = env.parse("Hello {{ name }}");
|
||||
inja::Template t2 = env.parse("{% if is_happy %}{{ name }}{% else %}{{ city }}{% endif %}");
|
||||
inja::Template t3 = env.parse("{% if at(name, test) %}{{ name }}{% else %}{{ city }}{{ upper(city) }}{% endif %}");
|
||||
|
||||
CHECK(t1.count_variables() == 1);
|
||||
CHECK(t2.count_variables() == 3);
|
||||
CHECK(t3.count_variables() == 5);
|
||||
}
|
||||
|
||||
SUBCASE("whitespace control") {
|
||||
inja::Environment env;
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %}", data) == "Peter");
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif %} ", data) == " Peter ");
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif %}\n ", data) == " Peter\n ");
|
||||
CHECK(env.render("Test\n {%- if is_happy %}{{ name }}{% endif %} ", data) == "Test\nPeter ");
|
||||
CHECK(env.render(" {%+ if is_happy %}{{ name }}{% endif %}", data) == " Peter");
|
||||
CHECK(env.render(" {%- if is_happy %}{{ name }}{% endif -%} \n ", data) == "Peter");
|
||||
|
||||
// Nothing will be stripped if there are other characters before the start of the block.
|
||||
CHECK(env.render(". {%- if is_happy %}{{ name }}{% endif -%}\n", data) == ". Peter");
|
||||
|
||||
env.set_lstrip_blocks(true);
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif %}", data) == "Peter");
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif %} ", data) == "Peter ");
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif -%} ", data) == "Peter");
|
||||
CHECK(env.render(" {%+ if is_happy %}{{ name }}{% endif %}", data) == " Peter");
|
||||
CHECK(env.render("\n {%+ if is_happy %}{{ name }}{% endif -%} ", data) == "\n Peter");
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %}\n", data) == "Peter\n");
|
||||
|
||||
env.set_trim_blocks(true);
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %}", data) == "Peter");
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %}\n", data) == "Peter");
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %} \n.", data) == "Peter.");
|
||||
CHECK(env.render("{%- if is_happy %}{{ name }}{% endif -%} \n.", data) == "Peter.");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("other syntax") {
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "Brunswick";
|
||||
data["age"] = 29;
|
||||
data["names"] = {"Jeff", "Seb"};
|
||||
data["brother"]["name"] = "Chris";
|
||||
data["brother"]["daughters"] = {"Maria", "Helen"};
|
||||
data["brother"]["daughter0"] = {{"name", "Maria"}};
|
||||
data["is_happy"] = true;
|
||||
|
||||
SUBCASE("other expression syntax") {
|
||||
inja::Environment env;
|
||||
|
||||
CHECK(env.render("Hello {{ name }}!", data) == "Hello Peter!");
|
||||
|
||||
env.set_expression("(&", "&)");
|
||||
|
||||
CHECK(env.render("Hello {{ name }}!", data) == "Hello {{ name }}!");
|
||||
CHECK(env.render("Hello (& name &)!", data) == "Hello Peter!");
|
||||
}
|
||||
|
||||
SUBCASE("other comment syntax") {
|
||||
inja::Environment env;
|
||||
env.set_comment("(&", "&)");
|
||||
|
||||
CHECK(env.render("Hello {# Test #}", data) == "Hello {# Test #}");
|
||||
CHECK(env.render("Hello (& Test &)", data) == "Hello ");
|
||||
}
|
||||
|
||||
SUBCASE("multiple changes") {
|
||||
inja::Environment env;
|
||||
env.set_line_statement("$$");
|
||||
env.set_expression("<%", "%>");
|
||||
|
||||
std::string string_template = R"DELIM(Hello <%name%>
|
||||
$$ if name == "Peter"
|
||||
You really are <%name%>
|
||||
$$ endif
|
||||
)DELIM";
|
||||
|
||||
CHECK(env.render(string_template, data) == "Hello Peter\n You really are Peter\n");
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
|
||||
#include "doctest/doctest.h"
|
||||
#include "inja/inja.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
|
||||
TEST_CASE("source location") {
|
||||
std::string content = R"DELIM(Lorem Ipsum
|
||||
15
test/test.cpp
Normal file
15
test/test.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2020 Pantor. All rights reserved.
|
||||
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
|
||||
#include "doctest/doctest.h"
|
||||
#include "inja/inja.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
const std::string test_file_directory {"../test/data/"};
|
||||
|
||||
#include "test-files.cpp"
|
||||
#include "test-functions.cpp"
|
||||
#include "test-renderer.cpp"
|
||||
#include "test-units.cpp"
|
||||
@@ -1,516 +0,0 @@
|
||||
// Copyright (c) 2019 Pantor. All rights reserved.
|
||||
|
||||
#include "doctest/doctest.h"
|
||||
#include "inja/inja.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
TEST_CASE("dot-to-pointer") {
|
||||
std::string buffer;
|
||||
CHECK(inja::convert_dot_to_json_pointer("test", buffer) == "/test");
|
||||
CHECK(inja::convert_dot_to_json_pointer("guests.2", buffer) == "/guests/2");
|
||||
CHECK(inja::convert_dot_to_json_pointer("person.names.surname", buffer) == "/person/names/surname");
|
||||
}
|
||||
|
||||
TEST_CASE("types") {
|
||||
inja::Environment env;
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "Brunswick";
|
||||
data["age"] = 29;
|
||||
data["names"] = {"Jeff", "Seb"};
|
||||
data["brother"]["name"] = "Chris";
|
||||
data["brother"]["daughters"] = {"Maria", "Helen"};
|
||||
data["brother"]["daughter0"] = {{"name", "Maria"}};
|
||||
data["is_happy"] = true;
|
||||
data["is_sad"] = false;
|
||||
data["relatives"]["mother"] = "Maria";
|
||||
data["relatives"]["brother"] = "Chris";
|
||||
data["relatives"]["sister"] = "Jenny";
|
||||
data["vars"] = {2, 3, 4, 0, -1, -2, -3};
|
||||
|
||||
SUBCASE("basic") {
|
||||
CHECK(env.render("", data) == "");
|
||||
CHECK(env.render("Hello World!", data) == "Hello World!");
|
||||
}
|
||||
|
||||
SUBCASE("variables") {
|
||||
CHECK(env.render("Hello {{ name }}!", data) == "Hello Peter!");
|
||||
CHECK(env.render("{{ name }}", data) == "Peter");
|
||||
CHECK(env.render("{{name}}", data) == "Peter");
|
||||
CHECK(env.render("{{ name }} is {{ age }} years old.", data) == "Peter is 29 years old.");
|
||||
CHECK(env.render("Hello {{ name }}! I come from {{ city }}.", data) == "Hello Peter! I come from Brunswick.");
|
||||
CHECK(env.render("Hello {{ names.1 }}!", data) == "Hello Seb!");
|
||||
CHECK(env.render("Hello {{ brother.name }}!", data) == "Hello Chris!");
|
||||
CHECK(env.render("Hello {{ brother.daughter0.name }}!", data) == "Hello Maria!");
|
||||
CHECK(env.render("{{ \"{{ no_value }}\" }}", data) == "{{ no_value }}");
|
||||
|
||||
CHECK_THROWS_WITH(env.render("{{unknown}}", data), "[inja.exception.render_error] (at 1:3) variable 'unknown' not found");
|
||||
}
|
||||
|
||||
SUBCASE("comments") {
|
||||
CHECK(env.render("Hello{# This is a comment #}!", data) == "Hello!");
|
||||
CHECK(env.render("{# --- #Todo --- #}", data) == "");
|
||||
}
|
||||
|
||||
SUBCASE("loops") {
|
||||
CHECK(env.render("{% for name in names %}a{% endfor %}", data) == "aa");
|
||||
CHECK(env.render("Hello {% for name in names %}{{ name }} {% endfor %}!", data) == "Hello Jeff Seb !");
|
||||
CHECK(env.render("Hello {% for name in names %}{{ loop.index }}: {{ name }}, {% endfor %}!", data) ==
|
||||
"Hello 0: Jeff, 1: Seb, !");
|
||||
CHECK(env.render("{% for type, name in relatives %}{{ loop.index1 }}: {{ type }}: {{ name }}{% if loop.is_last == "
|
||||
"false %}, {% endif %}{% endfor %}",
|
||||
data) == "1: brother: Chris, 2: mother: Maria, 3: sister: Jenny");
|
||||
CHECK(env.render("{% for v in vars %}{% if v > 0 %}+{% endif %}{% endfor %}", data) == "+++");
|
||||
CHECK(env.render(
|
||||
"{% for name in names %}{{ loop.index }}: {{ name }}{% if not loop.is_last %}, {% endif %}{% endfor %}!",
|
||||
data) == "0: Jeff, 1: Seb!");
|
||||
CHECK(env.render("{% for name in names %}{{ loop.index }}: {{ name }}{% if loop.is_last == false %}, {% endif %}{% "
|
||||
"endfor %}!",
|
||||
data) == "0: Jeff, 1: Seb!");
|
||||
|
||||
CHECK(env.render("{% for name in {} %}a{% endfor %}", data) == "");
|
||||
|
||||
CHECK_THROWS_WITH(env.render("{% for name ins names %}a{% endfor %}", data),
|
||||
"[inja.exception.parser_error] (at 1:13) expected 'in', got 'ins'");
|
||||
CHECK_THROWS_WITH(env.render("{% for name in empty_loop %}a{% endfor %}", data),
|
||||
"[inja.exception.render_error] (at 1:16) variable 'empty_loop' not found");
|
||||
// CHECK_THROWS_WITH( env.render("{% for name in relatives %}{{ name }}{% endfor %}", data),
|
||||
// "[inja.exception.json_error] [json.exception.type_error.302] type must be array, but is object" );
|
||||
}
|
||||
|
||||
SUBCASE("nested loops") {
|
||||
auto ldata = json::parse(
|
||||
R"DELIM(
|
||||
{ "outer" : [
|
||||
{ "inner" : [
|
||||
{ "in2" : [ 1, 2 ] },
|
||||
{ "in2" : []},
|
||||
{ "in2" : []}
|
||||
]
|
||||
},
|
||||
{ "inner" : [] },
|
||||
{ "inner" : [
|
||||
{ "in2" : [ 3, 4 ] },
|
||||
{ "in2" : [ 5, 6 ] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)DELIM");
|
||||
CHECK(env.render(R"DELIM(
|
||||
{% for o in outer %}{% for i in o.inner %}{{loop.parent.index}}:{{loop.index}}::{{loop.parent.is_last}}
|
||||
{% for ii in i.in2%}{{ii}},{%endfor%}
|
||||
{%endfor%}{%endfor%}
|
||||
)DELIM",
|
||||
ldata) == "\n0:0::false\n1,2,\n0:1::false\n\n0:2::false\n\n2:0::true\n3,4,\n2:1::true\n5,6,\n\n");
|
||||
}
|
||||
|
||||
SUBCASE("conditionals") {
|
||||
CHECK(env.render("{% if is_happy %}Yeah!{% endif %}", data) == "Yeah!");
|
||||
CHECK(env.render("{% if is_sad %}Yeah!{% endif %}", data) == "");
|
||||
CHECK(env.render("{% if is_sad %}Yeah!{% else %}Nooo...{% endif %}", data) == "Nooo...");
|
||||
CHECK(env.render("{% if age == 29 %}Right{% else %}Wrong{% endif %}", data) == "Right");
|
||||
CHECK(env.render("{% if age > 29 %}Right{% else %}Wrong{% endif %}", data) == "Wrong");
|
||||
CHECK(env.render("{% if age <= 29 %}Right{% else %}Wrong{% endif %}", data) == "Right");
|
||||
CHECK(env.render("{% if age != 28 %}Right{% else %}Wrong{% endif %}", data) == "Right");
|
||||
CHECK(env.render("{% if age >= 30 %}Right{% else %}Wrong{% endif %}", data) == "Wrong");
|
||||
CHECK(env.render("{% if age in [28, 29, 30] %}True{% endif %}", data) == "True");
|
||||
CHECK(env.render("{% if age == 28 %}28{% else if age == 29 %}29{% endif %}", data) == "29");
|
||||
CHECK(env.render("{% if age == 26 %}26{% else if age == 27 %}27{% else if age == 28 %}28{% else %}29{% endif %}",
|
||||
data) == "29");
|
||||
CHECK(env.render("{% if age == 25 %}+{% endif %}{% if age == 29 %}+{% else %}-{% endif %}", data) == "+");
|
||||
|
||||
CHECK_THROWS_WITH(env.render("{% if is_happy %}{% if is_happy %}{% endif %}", data),
|
||||
"[inja.exception.parser_error] (at 1:46) unmatched if");
|
||||
CHECK_THROWS_WITH(env.render("{% if is_happy %}{% else if is_happy %}{% end if %}", data),
|
||||
"[inja.exception.parser_error] (at 1:43) expected statement, got 'end'");
|
||||
}
|
||||
|
||||
SUBCASE("line statements") {
|
||||
CHECK(env.render(R"(## if is_happy
|
||||
Yeah!
|
||||
## endif)",
|
||||
data) == R"(Yeah!
|
||||
)");
|
||||
|
||||
CHECK(env.render(R"(## if is_happy
|
||||
## if is_happy
|
||||
Yeah!
|
||||
## endif
|
||||
## endif )",
|
||||
data) == R"(Yeah!
|
||||
)");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("functions") {
|
||||
inja::Environment env;
|
||||
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "New York";
|
||||
data["names"] = {"Jeff", "Seb", "Peter", "Tom"};
|
||||
data["temperature"] = 25.6789;
|
||||
data["brother"]["name"] = "Chris";
|
||||
data["brother"]["daughters"] = {"Maria", "Helen"};
|
||||
data["property"] = "name";
|
||||
data["age"] = 29;
|
||||
data["i"] = 1;
|
||||
data["is_happy"] = true;
|
||||
data["is_sad"] = false;
|
||||
data["vars"] = {2, 3, 4, 0, -1, -2, -3};
|
||||
|
||||
SUBCASE("upper") {
|
||||
CHECK(env.render("{{ upper(name) }}", data) == "PETER");
|
||||
CHECK(env.render("{{ upper( name ) }}", data) == "PETER");
|
||||
CHECK(env.render("{{ upper(city) }}", data) == "NEW YORK");
|
||||
CHECK(env.render("{{ upper(upper(name)) }}", data) == "PETER");
|
||||
// CHECK_THROWS_WITH( env.render("{{ upper(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be string, but is number" ); CHECK_THROWS_WITH( env.render("{{
|
||||
// upper(true) }}", data), "[inja.exception.json_error] [json.exception.type_error.302] type must be string, but is
|
||||
// boolean" );
|
||||
}
|
||||
|
||||
SUBCASE("lower") {
|
||||
CHECK(env.render("{{ lower(name) }}", data) == "peter");
|
||||
CHECK(env.render("{{ lower(city) }}", data) == "new york");
|
||||
// CHECK_THROWS_WITH( env.render("{{ lower(5.45) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be string, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("range") {
|
||||
CHECK(env.render("{{ range(2) }}", data) == "[0,1]");
|
||||
CHECK(env.render("{{ range(4) }}", data) == "[0,1,2,3]");
|
||||
// CHECK_THROWS_WITH( env.render("{{ range(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("length") {
|
||||
CHECK(env.render("{{ length(names) }}", data) == "4"); // Length of array
|
||||
CHECK(env.render("{{ length(name) }}", data) == "5"); // Length of string
|
||||
// CHECK_THROWS_WITH( env.render("{{ length(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("sort") {
|
||||
CHECK(env.render("{{ sort([3, 2, 1]) }}", data) == "[1,2,3]");
|
||||
CHECK(env.render("{{ sort([\"bob\", \"charlie\", \"alice\"]) }}", data) == "[\"alice\",\"bob\",\"charlie\"]");
|
||||
// CHECK_THROWS_WITH( env.render("{{ sort(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("at") {
|
||||
CHECK(env.render("{{ at(names, 0) }}", data) == "Jeff");
|
||||
CHECK(env.render("{{ at(names, i) }}", data) == "Seb");
|
||||
}
|
||||
|
||||
SUBCASE("first") {
|
||||
CHECK(env.render("{{ first(names) }}", data) == "Jeff");
|
||||
// CHECK_THROWS_WITH( env.render("{{ first(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("last") {
|
||||
CHECK(env.render("{{ last(names) }}", data) == "Tom");
|
||||
// CHECK_THROWS_WITH( env.render("{{ last(5) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is number" );
|
||||
}
|
||||
|
||||
SUBCASE("round") {
|
||||
CHECK(env.render("{{ round(4, 0) }}", data) == "4.0");
|
||||
CHECK(env.render("{{ round(temperature, 2) }}", data) == "25.68");
|
||||
// CHECK_THROWS_WITH( env.render("{{ round(name, 2) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("divisibleBy") {
|
||||
CHECK(env.render("{{ divisibleBy(50, 5) }}", data) == "true");
|
||||
CHECK(env.render("{{ divisibleBy(12, 3) }}", data) == "true");
|
||||
CHECK(env.render("{{ divisibleBy(11, 3) }}", data) == "false");
|
||||
// CHECK_THROWS_WITH( env.render("{{ divisibleBy(name, 2) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("odd") {
|
||||
CHECK(env.render("{{ odd(11) }}", data) == "true");
|
||||
CHECK(env.render("{{ odd(12) }}", data) == "false");
|
||||
// CHECK_THROWS_WITH( env.render("{{ odd(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("even") {
|
||||
CHECK(env.render("{{ even(11) }}", data) == "false");
|
||||
CHECK(env.render("{{ even(12) }}", data) == "true");
|
||||
// CHECK_THROWS_WITH( env.render("{{ even(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be number, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("max") {
|
||||
CHECK(env.render("{{ max([1, 2, 3]) }}", data) == "3");
|
||||
CHECK(env.render("{{ max([-5.2, 100.2, 2.4]) }}", data) == "100.2");
|
||||
// CHECK_THROWS_WITH( env.render("{{ max(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("min") {
|
||||
CHECK(env.render("{{ min([1, 2, 3]) }}", data) == "1");
|
||||
CHECK(env.render("{{ min([-5.2, 100.2, 2.4]) }}", data) == "-5.2");
|
||||
// CHECK_THROWS_WITH( env.render("{{ min(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("float") {
|
||||
CHECK(env.render("{{ float(\"2.2\") == 2.2 }}", data) == "true");
|
||||
CHECK(env.render("{{ float(\"-1.25\") == -1.25 }}", data) == "true");
|
||||
// CHECK_THROWS_WITH( env.render("{{ max(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("int") {
|
||||
CHECK(env.render("{{ int(\"2\") == 2 }}", data) == "true");
|
||||
CHECK(env.render("{{ int(\"-1.25\") == -1 }}", data) == "true");
|
||||
// CHECK_THROWS_WITH( env.render("{{ max(name) }}", data), "[inja.exception.json_error]
|
||||
// [json.exception.type_error.302] type must be array, but is string" );
|
||||
}
|
||||
|
||||
SUBCASE("default") {
|
||||
CHECK(env.render("{{ default(11, 0) }}", data) == "11");
|
||||
CHECK(env.render("{{ default(nothing, 0) }}", data) == "0");
|
||||
CHECK(env.render("{{ default(name, \"nobody\") }}", data) == "Peter");
|
||||
CHECK(env.render("{{ default(surname, \"nobody\") }}", data) == "nobody");
|
||||
CHECK(env.render("{{ default(surname, \"{{ surname }}\") }}", data) == "{{ surname }}");
|
||||
CHECK_THROWS_WITH(env.render("{{ default(surname, lastname) }}", data),
|
||||
"[inja.exception.render_error] (at 1:21) variable 'lastname' not found");
|
||||
}
|
||||
|
||||
SUBCASE("exists") {
|
||||
CHECK(env.render("{{ exists(\"name\") }}", data) == "true");
|
||||
CHECK(env.render("{{ exists(\"zipcode\") }}", data) == "false");
|
||||
CHECK(env.render("{{ exists(name) }}", data) == "false");
|
||||
CHECK(env.render("{{ exists(property) }}", data) == "true");
|
||||
}
|
||||
|
||||
SUBCASE("existsIn") {
|
||||
CHECK(env.render("{{ existsIn(brother, \"name\") }}", data) == "true");
|
||||
CHECK(env.render("{{ existsIn(brother, \"parents\") }}", data) == "false");
|
||||
CHECK(env.render("{{ existsIn(brother, property) }}", data) == "true");
|
||||
CHECK(env.render("{{ existsIn(brother, name) }}", data) == "false");
|
||||
CHECK_THROWS_WITH(env.render("{{ existsIn(sister, \"lastname\") }}", data),
|
||||
"[inja.exception.render_error] (at 1:13) variable 'sister' not found");
|
||||
CHECK_THROWS_WITH(env.render("{{ existsIn(brother, sister) }}", data),
|
||||
"[inja.exception.render_error] (at 1:22) variable 'sister' not found");
|
||||
}
|
||||
|
||||
SUBCASE("isType") {
|
||||
CHECK(env.render("{{ isBoolean(is_happy) }}", data) == "true");
|
||||
CHECK(env.render("{{ isBoolean(vars) }}", data) == "false");
|
||||
CHECK(env.render("{{ isNumber(age) }}", data) == "true");
|
||||
CHECK(env.render("{{ isNumber(name) }}", data) == "false");
|
||||
CHECK(env.render("{{ isInteger(age) }}", data) == "true");
|
||||
CHECK(env.render("{{ isInteger(is_happy) }}", data) == "false");
|
||||
CHECK(env.render("{{ isFloat(temperature) }}", data) == "true");
|
||||
CHECK(env.render("{{ isFloat(age) }}", data) == "false");
|
||||
CHECK(env.render("{{ isObject(brother) }}", data) == "true");
|
||||
CHECK(env.render("{{ isObject(vars) }}", data) == "false");
|
||||
CHECK(env.render("{{ isArray(vars) }}", data) == "true");
|
||||
CHECK(env.render("{{ isArray(name) }}", data) == "false");
|
||||
CHECK(env.render("{{ isString(name) }}", data) == "true");
|
||||
CHECK(env.render("{{ isString(names) }}", data) == "false");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("callbacks") {
|
||||
inja::Environment env;
|
||||
json data;
|
||||
data["age"] = 28;
|
||||
|
||||
env.add_callback("double", 1, [](inja::Arguments &args) {
|
||||
int number = args.at(0)->get<int>();
|
||||
return 2 * number;
|
||||
});
|
||||
|
||||
env.add_callback("half", 1, [](inja::Arguments args) {
|
||||
int number = args.at(0)->get<int>();
|
||||
return number / 2;
|
||||
});
|
||||
|
||||
std::string greet = "Hello";
|
||||
env.add_callback("double-greetings", 0, [greet](inja::Arguments args) { return greet + " " + greet + "!"; });
|
||||
|
||||
env.add_callback("multiply", 2, [](inja::Arguments args) {
|
||||
double number1 = args.at(0)->get<double>();
|
||||
auto number2 = args.at(1)->get<double>();
|
||||
return number1 * number2;
|
||||
});
|
||||
|
||||
env.add_callback("multiply", 3, [](inja::Arguments args) {
|
||||
double number1 = args.at(0)->get<double>();
|
||||
double number2 = args.at(1)->get<double>();
|
||||
double number3 = args.at(2)->get<double>();
|
||||
return number1 * number2 * number3;
|
||||
});
|
||||
|
||||
env.add_callback("multiply", 0, [](inja::Arguments args) { return 1.0; });
|
||||
|
||||
CHECK(env.render("{{ double(age) }}", data) == "56");
|
||||
CHECK(env.render("{{ half(age) }}", data) == "14");
|
||||
CHECK(env.render("{{ double-greetings }}", data) == "Hello Hello!");
|
||||
CHECK(env.render("{{ double-greetings() }}", data) == "Hello Hello!");
|
||||
CHECK(env.render("{{ multiply(4, 5) }}", data) == "20.0");
|
||||
CHECK(env.render("{{ multiply(3, 4, 5) }}", data) == "60.0");
|
||||
CHECK(env.render("{{ multiply }}", data) == "1.0");
|
||||
}
|
||||
|
||||
TEST_CASE("combinations") {
|
||||
inja::Environment env;
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "Brunswick";
|
||||
data["age"] = 29;
|
||||
data["names"] = {"Jeff", "Seb"};
|
||||
data["brother"]["name"] = "Chris";
|
||||
data["brother"]["daughters"] = {"Maria", "Helen"};
|
||||
data["brother"]["daughter0"] = {{"name", "Maria"}};
|
||||
data["is_happy"] = true;
|
||||
|
||||
CHECK(env.render("{% if upper(\"Peter\") == \"PETER\" %}TRUE{% endif %}", data) == "TRUE");
|
||||
CHECK(env.render("{% if lower(upper(name)) == \"peter\" %}TRUE{% endif %}", data) == "TRUE");
|
||||
CHECK(env.render("{% for i in range(4) %}{{ loop.index1 }}{% endfor %}", data) == "1234");
|
||||
}
|
||||
|
||||
TEST_CASE("templates") {
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "Brunswick";
|
||||
data["is_happy"] = true;
|
||||
|
||||
SUBCASE("reuse") {
|
||||
inja::Environment env;
|
||||
inja::Template temp = env.parse("{% if is_happy %}{{ name }}{% else %}{{ city }}{% endif %}");
|
||||
|
||||
CHECK(env.render(temp, data) == "Peter");
|
||||
|
||||
data["is_happy"] = false;
|
||||
|
||||
CHECK(env.render(temp, data) == "Brunswick");
|
||||
}
|
||||
|
||||
SUBCASE("include") {
|
||||
inja::Environment env;
|
||||
inja::Template t1 = env.parse("Hello {{ name }}");
|
||||
env.include_template("greeting", t1);
|
||||
|
||||
inja::Template t2 = env.parse("{% include \"greeting\" %}!");
|
||||
CHECK(env.render(t2, data) == "Hello Peter!");
|
||||
CHECK_THROWS_WITH(env.parse("{% include \"does-not-exist\" %}!"),
|
||||
"[inja.exception.file_error] failed accessing file at 'does-not-exist'");
|
||||
}
|
||||
|
||||
SUBCASE("include-in-loop") {
|
||||
json loop_data;
|
||||
loop_data["cities"] = json::array({{{"name", "Munich"}}, {{"name", "New York"}}});
|
||||
|
||||
inja::Environment env;
|
||||
env.include_template("city.tpl", env.parse("{{ loop.index }}:{{ city.name }};"));
|
||||
|
||||
CHECK(env.render("{% for city in cities %}{% include \"city.tpl\" %}{% endfor %}", loop_data) ==
|
||||
"0:Munich;1:New York;");
|
||||
}
|
||||
|
||||
SUBCASE("count variables") {
|
||||
inja::Environment env;
|
||||
inja::Template t1 = env.parse("Hello {{ name }}");
|
||||
inja::Template t2 = env.parse("{% if is_happy %}{{ name }}{% else %}{{ city }}{% endif %}");
|
||||
inja::Template t3 = env.parse("{% if at(name, test) %}{{ name }}{% else %}{{ city }}{{ upper(city) }}{% endif %}");
|
||||
|
||||
CHECK(t1.count_variables() == 1);
|
||||
CHECK(t2.count_variables() == 3);
|
||||
CHECK(t3.count_variables() == 5);
|
||||
}
|
||||
|
||||
SUBCASE("whitespace control") {
|
||||
inja::Environment env;
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %}", data) == "Peter");
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif %} ", data) == " Peter ");
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif %}\n ", data) == " Peter\n ");
|
||||
CHECK(env.render("Test\n {%- if is_happy %}{{ name }}{% endif %} ", data) == "Test\nPeter ");
|
||||
CHECK(env.render(" {%+ if is_happy %}{{ name }}{% endif %}", data) == " Peter");
|
||||
CHECK(env.render(" {%- if is_happy %}{{ name }}{% endif -%} \n ", data) == "Peter");
|
||||
|
||||
// Nothing will be stripped if there are other characters before the start of the block.
|
||||
CHECK(env.render(". {%- if is_happy %}{{ name }}{% endif -%}\n", data) == ". Peter");
|
||||
|
||||
env.set_lstrip_blocks(true);
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif %}", data) == "Peter");
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif %} ", data) == "Peter ");
|
||||
CHECK(env.render(" {% if is_happy %}{{ name }}{% endif -%} ", data) == "Peter");
|
||||
CHECK(env.render(" {%+ if is_happy %}{{ name }}{% endif %}", data) == " Peter");
|
||||
CHECK(env.render("\n {%+ if is_happy %}{{ name }}{% endif -%} ", data) == "\n Peter");
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %}\n", data) == "Peter\n");
|
||||
|
||||
env.set_trim_blocks(true);
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %}", data) == "Peter");
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %}\n", data) == "Peter");
|
||||
CHECK(env.render("{% if is_happy %}{{ name }}{% endif %} \n.", data) == "Peter.");
|
||||
CHECK(env.render("{%- if is_happy %}{{ name }}{% endif -%} \n.", data) == "Peter.");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("other syntax") {
|
||||
json data;
|
||||
data["name"] = "Peter";
|
||||
data["city"] = "Brunswick";
|
||||
data["age"] = 29;
|
||||
data["names"] = {"Jeff", "Seb"};
|
||||
data["brother"]["name"] = "Chris";
|
||||
data["brother"]["daughters"] = {"Maria", "Helen"};
|
||||
data["brother"]["daughter0"] = {{"name", "Maria"}};
|
||||
data["is_happy"] = true;
|
||||
|
||||
SUBCASE("variables") {
|
||||
inja::Environment env;
|
||||
env.set_element_notation(inja::ElementNotation::Pointer);
|
||||
|
||||
CHECK(env.render("{{ name }}", data) == "Peter");
|
||||
CHECK(env.render("Hello {{ names/1 }}!", data) == "Hello Seb!");
|
||||
CHECK(env.render("Hello {{ brother/name }}!", data) == "Hello Chris!");
|
||||
CHECK(env.render("Hello {{ brother/daughter0/name }}!", data) == "Hello Maria!");
|
||||
|
||||
CHECK_THROWS_WITH(env.render("{{unknown/name}}", data),
|
||||
"[inja.exception.render_error] (at 1:3) variable 'unknown/name' not found");
|
||||
}
|
||||
|
||||
SUBCASE("other expression syntax") {
|
||||
inja::Environment env;
|
||||
|
||||
CHECK(env.render("Hello {{ name }}!", data) == "Hello Peter!");
|
||||
|
||||
env.set_expression("(&", "&)");
|
||||
|
||||
CHECK(env.render("Hello {{ name }}!", data) == "Hello {{ name }}!");
|
||||
CHECK(env.render("Hello (& name &)!", data) == "Hello Peter!");
|
||||
}
|
||||
|
||||
SUBCASE("other comment syntax") {
|
||||
inja::Environment env;
|
||||
env.set_comment("(&", "&)");
|
||||
|
||||
CHECK(env.render("Hello {# Test #}", data) == "Hello {# Test #}");
|
||||
CHECK(env.render("Hello (& Test &)", data) == "Hello ");
|
||||
}
|
||||
|
||||
SUBCASE("multiple changes") {
|
||||
inja::Environment env;
|
||||
env.set_line_statement("$$");
|
||||
env.set_expression("<%", "%>");
|
||||
|
||||
std::string string_template = R"DELIM(Hello <%name%>
|
||||
$$ if name == "Peter"
|
||||
You really are <%name%>
|
||||
$$ endif
|
||||
)DELIM";
|
||||
|
||||
CHECK(env.render(string_template, data) == "Hello Peter\n You really are Peter\n");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user