From 889da4da4fc48fc23b9b449bad202c265d0e0449 Mon Sep 17 00:00:00 2001 From: pantor Date: Mon, 3 Aug 2020 21:29:05 +0200 Subject: [PATCH] add set statements --- CMakeLists.txt | 2 +- README.md | 9 ++++++++- include/inja/inja.hpp | 2 +- include/inja/node.hpp | 14 ++++++++++++++ include/inja/parser.hpp | 23 +++++++++++++++++++++++ include/inja/renderer.hpp | 32 ++++++++++++++++++-------------- include/inja/statistics.hpp | 2 ++ test/test-renderer.cpp | 7 +++++++ 8 files changed, 74 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8769b83f..0aee13ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.5) -project(inja LANGUAGES CXX VERSION 3.0.0) +project(inja LANGUAGES CXX VERSION 3.1.0) option(INJA_USE_EMBEDDED_JSON "Use the shipped json header if not available on the system" ON) diff --git a/README.md b/README.md index 8f3a0a85..bd2b14b1 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ inja::render("Hello {{ name }}!", data); // Returns "Hello world!" ## Integration -Inja is a headers only library, which can be downloaded from the [releases](https://github.com/pantor/inja/releases) or directly from the `include/` or `single_include/` folder. Inja uses `nlohmann/json.hpp` as its single dependency, so make sure it can be included from `inja.hpp`. json can be downloaded [here](https://github.com/nlohmann/json/releases). Then integration is as easy as: +Inja is a headers only library, which can be downloaded from the [releases](https://github.com/pantor/inja/releases) or directly from the `include/` or `single_include/` folder. Inja uses `nlohmann/json.hpp` (>= v3.8.0) as its single dependency, so make sure it can be included from `inja.hpp`. json can be downloaded [here](https://github.com/nlohmann/json/releases). Then integration is as easy as: ```.cpp #include @@ -308,6 +308,13 @@ render("Hello{# Todo #}!", data); // "Hello!" ``` +### Set Statements + +Variables can also be defined within the template using the set statment. +```.cpp +render("{% set new_hour=23 %}{{ new_hour }}pm", data); // "23pm" +``` + ## Supported compilers Inja uses `string_view` from C++17, but includes the [polyfill](https://github.com/martinmoene/string-view-lite) from martinmoene. This way, the minimum version is C++11. Currently, the following compilers are tested: diff --git a/include/inja/inja.hpp b/include/inja/inja.hpp index c0509dc3..3075b517 100644 --- a/include/inja/inja.hpp +++ b/include/inja/inja.hpp @@ -1,4 +1,4 @@ -// Copyright (c) 2019 Pantor. All rights reserved. +// Copyright (c) 2020 Pantor. All rights reserved. #ifndef INCLUDE_INJA_INJA_HPP_ #define INCLUDE_INJA_INJA_HPP_ diff --git a/include/inja/node.hpp b/include/inja/node.hpp index 19314ebe..84a99cae 100644 --- a/include/inja/node.hpp +++ b/include/inja/node.hpp @@ -28,6 +28,7 @@ class ForArrayStatementNode; class ForObjectStatementNode; class IfStatementNode; class IncludeStatementNode; +class SetStatementNode; class NodeVisitor { @@ -45,6 +46,7 @@ class NodeVisitor { virtual void visit(const ForObjectStatementNode& node) = 0; virtual void visit(const IfStatementNode& node) = 0; virtual void visit(const IncludeStatementNode& node) = 0; + virtual void visit(const SetStatementNode& node) = 0; }; /*! @@ -311,6 +313,18 @@ class IncludeStatementNode : public StatementNode { }; }; +class SetStatementNode : public StatementNode { +public: + std::string key; + ExpressionListNode expression; + + explicit SetStatementNode(const std::string& key, size_t pos) : StatementNode(pos), key(key) { } + + void accept(NodeVisitor& v) const { + v.visit(*this); + }; +}; + } // namespace inja #endif // INCLUDE_INJA_NODE_HPP_ diff --git a/include/inja/parser.hpp b/include/inja/parser.hpp index 6441910a..0cc51e58 100644 --- a/include/inja/parser.hpp +++ b/include/inja/parser.hpp @@ -449,6 +449,29 @@ class Parser { get_next_token(); + } else if (tok.text == static_cast("set")) { + get_next_token(); + + if (tok.kind != Token::Kind::Id) { + throw_parser_error("expected variable name, got '" + tok.describe() + "'"); + } + + std::string key = static_cast(tok.text); + get_next_token(); + + auto set_statement_node = std::make_shared(key, tok.text.data() - tmpl.content.c_str()); + current_block->nodes.emplace_back(set_statement_node); + current_expression_list = &set_statement_node->expression; + + if (tok.text != static_cast("=")) { + throw_parser_error("expected '=', got '" + tok.describe() + "'"); + } + get_next_token(); + + if (!parse_expression(tmpl, closing)) { + return false; + } + } else { return false; } diff --git a/include/inja/renderer.hpp b/include/inja/renderer.hpp index d62bdb67..5c0ec109 100644 --- a/include/inja/renderer.hpp +++ b/include/inja/renderer.hpp @@ -33,8 +33,8 @@ class Renderer : public NodeVisitor { const json *json_input; std::ostream *output_stream; - json json_loop_data; - json* current_loop_data = &json_loop_data["loop"]; + json json_additional_data; + json* current_loop_data = &json_additional_data["loop"]; std::vector> json_tmp_stack; std::stack json_eval_stack; @@ -161,8 +161,8 @@ class Renderer : public NodeVisitor { try { // First try to evaluate as a loop variable - if (json_loop_data.contains(ptr)) { - json_eval_stack.push(&json_loop_data.at(ptr)); + if (json_additional_data.contains(ptr)) { + json_eval_stack.push(&json_additional_data.at(ptr)); } else { json_eval_stack.push(&json_input->at(ptr)); } @@ -502,7 +502,7 @@ class Renderer : public NodeVisitor { size_t index = 0; for (auto it = result->begin(); it != result->end(); ++it) { - json_loop_data[static_cast(node.value)] = *it; + json_additional_data[static_cast(node.value)] = *it; (*current_loop_data)["index"] = index; (*current_loop_data)["index1"] = index + 1; @@ -513,12 +513,12 @@ class Renderer : public NodeVisitor { ++index; } - json_loop_data[static_cast(node.value)].clear(); + json_additional_data[static_cast(node.value)].clear(); if (!(*current_loop_data)["parent"].empty()) { auto tmp = (*current_loop_data)["parent"]; *current_loop_data = std::move(tmp); } else { - current_loop_data = &json_loop_data["loop"]; + current_loop_data = &json_additional_data["loop"]; } } @@ -534,8 +534,8 @@ class Renderer : public NodeVisitor { size_t index = 0; for (auto it = result->begin(); it != result->end(); ++it) { - json_loop_data[static_cast(node.key)] = it.key(); - json_loop_data[static_cast(node.value)] = it.value(); + json_additional_data[static_cast(node.key)] = it.key(); + json_additional_data[static_cast(node.value)] = it.value(); (*current_loop_data)["index"] = index; (*current_loop_data)["index1"] = index + 1; @@ -546,12 +546,12 @@ class Renderer : public NodeVisitor { ++index; } - json_loop_data[static_cast(node.key)].clear(); - json_loop_data[static_cast(node.value)].clear(); + json_additional_data[static_cast(node.key)].clear(); + json_additional_data[static_cast(node.value)].clear(); if (!(*current_loop_data)["parent"].empty()) { *current_loop_data = std::move((*current_loop_data)["parent"]); } else { - current_loop_data = &json_loop_data["loop"]; + current_loop_data = &json_additional_data["loop"]; } } @@ -569,12 +569,16 @@ class Renderer : public NodeVisitor { auto included_template_it = template_storage.find(node.file); if (included_template_it != template_storage.end()) { - sub_renderer.render_to(*output_stream, included_template_it->second, *json_input, &json_loop_data); + sub_renderer.render_to(*output_stream, included_template_it->second, *json_input, &json_additional_data); } else if (config.throw_at_missing_includes) { throw_renderer_error("include '" + node.file + "' not found", node); } } + void visit(const SetStatementNode& node) { + json_additional_data[node.key] = *eval_expression_list(node.expression); + } + public: Renderer(const RenderConfig& config, const TemplateStorage &template_storage, const FunctionStorage &function_storage) : config(config), template_storage(template_storage), function_storage(function_storage) { } @@ -584,7 +588,7 @@ class Renderer : public NodeVisitor { current_template = &tmpl; json_input = &data; if (loop_data) { - json_loop_data = *loop_data; + json_additional_data = *loop_data; } current_template->root.accept(*this); diff --git a/include/inja/statistics.hpp b/include/inja/statistics.hpp index c8be1b39..71fc719e 100644 --- a/include/inja/statistics.hpp +++ b/include/inja/statistics.hpp @@ -55,6 +55,8 @@ class StatisticsVisitor : public NodeVisitor { void visit(const IncludeStatementNode&) { } + void visit(const SetStatementNode&) { } + public: unsigned int variable_counter; diff --git a/test/test-renderer.cpp b/test/test-renderer.cpp index a25694e4..73451975 100644 --- a/test/test-renderer.cpp +++ b/test/test-renderer.cpp @@ -122,6 +122,13 @@ TEST_CASE("types") { "[inja.exception.parser_error] (at 1:43) expected statement, got 'end'"); } + SUBCASE("set statements") { + CHECK(env.render("{% set predefined=true %}{% if predefined %}a{% endif %}", data) == "a"); + CHECK(env.render("{% set predefined=false %}{% if predefined %}a{% endif %}", data) == ""); + CHECK_THROWS_WITH(env.render("{% if predefined %}{% endif %}", data), + "[inja.exception.render_error] (at 1:7) variable 'predefined' not found"); + } + SUBCASE("line statements") { CHECK(env.render(R""""(## if is_happy Yeah!