src / ferdios / libfinja

Open in Gitea

Initial commit

7 files changed +3356 −0

  • + CMakeLists.txt
  • + LICENSE
  • + README.md
  • + cmake/finjaConfig.cmake.in
  • + examples/example.cpp
  • + finja/finja.hpp
  • + tests/test_undefined.cpp
1commit cab8ed0a2f203c40c7869e87078db92594c9b16a
2Author: Sander <sander@sanderf.nl>
3Date: Fri Sep 4 16:43:38 2026 +0300
4
5 Initial commit
6
7diff --git a/CMakeLists.txt b/CMakeLists.txt
8new file mode 100644
9index 0000000..d8a395b
10--- /dev/null
11+++ b/CMakeLists.txt
12@@ -0,0 +1,72 @@
13+cmake_minimum_required(VERSION 3.20)
14+
15+project(finja VERSION 4.1.0 LANGUAGES CXX)
16+
17+if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
18+ message(FATAL_ERROR "supports only Linux")
19+endif()
20+
21+set(INJA_MAIN_PROJECT OFF)
22+if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
23+ set(INJA_MAIN_PROJECT ON)
24+endif()
25+
26+include(GNUInstallDirs)
27+include(CMakePackageConfigHelpers)
28+
29+set(CMAKE_CXX_STANDARD 20)
30+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
31+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static")
32+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static")
33+
34+option(INJA_INSTALL "Enable generation of install targets" ${INJA_MAIN_PROJECT})
35+
36+option(BUILD_TESTS "Build tests" OFF)
37+option(BUILD_EXAMPLE "Build example programs" OFF)
38+
39+add_library(finja INTERFACE)
40+
41+target_include_directories(finja INTERFACE
42+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
43+ $<INSTALL_INTERFACE:include>
44+)
45+
46+if(BUILD_TESTS)
47+ enable_testing()
48+ add_executable(test_undefined tests/test_undefined.cpp)
49+ target_link_libraries(test_undefined PRIVATE finja)
50+ add_test(NAME undefined_variables COMMAND test_undefined)
51+endif()
52+
53+if(BUILD_EXAMPLE)
54+ add_executable(finja_example examples/example.cpp)
55+ target_link_libraries(finja_example PRIVATE finja)
56+endif()
57+
58+if(INJA_INSTALL)
59+ install(FILES finja/finja.hpp
60+ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/finja/
61+ COMPONENT development
62+ )
63+
64+ install(TARGETS finja
65+ EXPORT finjaTargets
66+ COMPONENT development
67+ )
68+
69+ install(EXPORT finjaTargets
70+ FILE finjaTargets.cmake
71+ NAMESPACE finja::
72+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/finja
73+ )
74+
75+ configure_package_config_file(
76+ ${CMAKE_CURRENT_SOURCE_DIR}/cmake/finjaConfig.cmake.in
77+ ${CMAKE_CURRENT_BINARY_DIR}/finjaConfig.cmake
78+ INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/finja
79+ )
80+
81+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/finjaConfig.cmake
82+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/finja
83+ )
84+endif()
85\ No newline at end of file
86diff --git a/LICENSE b/LICENSE
87new file mode 100644
88index 0000000..e92b008
89--- /dev/null
90+++ b/LICENSE
91@@ -0,0 +1,21 @@
92+MIT License
93+
94+Copyright (c) 2018-2025 Lars Berscheid
95+
96+Permission is hereby granted, free of charge, to any person obtaining a copy
97+of this software and associated documentation files (the "Software"), to deal
98+in the Software without restriction, including without limitation the rights
99+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
100+copies of the Software, and to permit persons to whom the Software is
101+furnished to do so, subject to the following conditions:
102+
103+The above copyright notice and this permission notice shall be included in all
104+copies or substantial portions of the Software.
105+
106+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
107+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
108+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
109+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
110+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
111+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
112+SOFTWARE.
113diff --git a/README.md b/README.md
114new file mode 100644
115index 0000000..0f625f5
116--- /dev/null
117+++ b/README.md
118@@ -0,0 +1,7 @@
119+# libfinja
120+
121+A mix between jinja2 and golang templating for C++
122+
123+# License
124+
125+MIT
126\ No newline at end of file
127diff --git a/cmake/finjaConfig.cmake.in b/cmake/finjaConfig.cmake.in
128new file mode 100644
129index 0000000..19f64cc
130--- /dev/null
131+++ b/cmake/finjaConfig.cmake.in
132@@ -0,0 +1,5 @@
133+@PACKAGE_INIT@
134+
135+include("${CMAKE_CURRENT_LIST_DIR}/finjaTargets.cmake")
136+
137+check_required_components(inja)
138diff --git a/examples/example.cpp b/examples/example.cpp
139new file mode 100644
140index 0000000..19110f4
141--- /dev/null
142+++ b/examples/example.cpp
143@@ -0,0 +1,17 @@
144+#include <iostream>
145+
146+#include "finja/finja.hpp"
147+
148+using namespace finja;
149+
150+int main() {
151+ json data;
152+ data["name"] = "world";
153+
154+ std::string out = render("Hello {{ name }}!", data);
155+ std::string out2 = render("{{ if not name }}this should print{{ endif }}", nullptr);
156+ //
157+ printf("out: %s\n", out.c_str());
158+ printf("out2: %s\n", out2.c_str());
159+ return 0;
160+}
161diff --git a/finja/finja.hpp b/finja/finja.hpp
162new file mode 100644
163index 0000000..8b403ce
164--- /dev/null
165+++ b/finja/finja.hpp
166@@ -0,0 +1,3131 @@
167+/*
168+ ___ _ libfinja, fork of inja github.com/pantor/inja
169+ |_ _|_ __ (_) __ _ https://codeberg.org/ferdios/libfinja
170+ | || '_ \ | |/ _` | Licensed under the MIT License <http://opensource.org/licenses/MIT>.
171+ | || | | || | (_| |
172+f|___|_| |_|/ |\__,_| Copyright (c) 2018-2025 Lars Berscheid
173+ |__/ Copyright (c) 2026-* Sander Ferdinand
174+Permission is hereby granted, free of charge, to any person obtaining a copy
175+of this software and associated documentation files (the "Software"), to deal
176+in the Software without restriction, including without limitation the rights
177+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
178+copies of the Software, and to permit persons to whom the Software is
179+furnished to do so, subject to the following conditions:
180+The above copyright notice and this permission notice shall be included in all
181+copies or substantial portions of the Software.
182+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
183+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
184+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
185+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
186+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
187+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
188+SOFTWARE.
189+*/
190+
191+#ifndef INCLUDE_FINJA_FINJA_HPP_
192+#define INCLUDE_FINJA_FINJA_HPP_
193+
194+// #include "json.hpp"
195+#ifndef INCLUDE_FINJA_JSON_HPP_
196+#define INCLUDE_FINJA_JSON_HPP_
197+
198+#include <nlohmann/json.hpp>
199+
200+namespace finja {
201+#ifndef INJA_DATA_TYPE
202+using json = nlohmann::json;
203+#else
204+using json = INJA_DATA_TYPE;
205+#endif
206+} // namespace finja
207+
208+#endif // INCLUDE_FINJA_JSON_HPP_
209+
210+// #include "throw.hpp"
211+#ifndef INCLUDE_FINJA_THROW_HPP_
212+#define INCLUDE_FINJA_THROW_HPP_
213+
214+#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(INJA_NOEXCEPTION)
215+#ifndef INJA_THROW
216+#define INJA_THROW(exception) throw exception
217+#endif
218+#else
219+#include <cstdlib>
220+#ifndef INJA_THROW
221+#define INJA_THROW(exception) \
222+std::abort(); \
223+ std::ignore = exception
224+#endif
225+#ifndef INJA_NOEXCEPTION
226+#define INJA_NOEXCEPTION
227+#endif
228+#endif
229+
230+#endif // INCLUDE_FINJA_THROW_HPP_
231+
232+// #include "environment.hpp"
233+#ifndef INCLUDE_FINJA_ENVIRONMENT_HPP_
234+#define INCLUDE_FINJA_ENVIRONMENT_HPP_
235+
236+#include <filesystem>
237+#include <fstream>
238+#include <iostream>
239+#include <sstream>
240+#include <string>
241+#include <string_view>
242+static std::string CURRENT_FILENAME;
243+
244+// #include "json.hpp"
245+
246+// #include "config.hpp"
247+#ifndef INCLUDE_FINJA_CONFIG_HPP_
248+#define INCLUDE_FINJA_CONFIG_HPP_
249+
250+#include <filesystem>
251+#include <functional>
252+#include <string>
253+
254+// #include "template.hpp"
255+#ifndef INCLUDE_FINJA_TEMPLATE_HPP_
256+#define INCLUDE_FINJA_TEMPLATE_HPP_
257+
258+#include <map>
259+#include <memory>
260+#include <string>
261+
262+// #include "node.hpp"
263+#ifndef INCLUDE_FINJA_NODE_HPP_
264+#define INCLUDE_FINJA_NODE_HPP_
265+
266+#include <cstddef>
267+#include <memory>
268+#include <string>
269+#include <string_view>
270+#include <tuple>
271+#include <vector>
272+
273+// #include "function_storage.hpp"
274+#ifndef INCLUDE_FINJA_FUNCTION_STORAGE_HPP_
275+#define INCLUDE_FINJA_FUNCTION_STORAGE_HPP_
276+
277+#include <functional>
278+#include <map>
279+#include <string>
280+#include <string_view>
281+#include <utility>
282+#include <vector>
283+
284+// #include "json.hpp"
285+
286+
287+namespace finja {
288+
289+using Arguments = std::vector<const json*>;
290+using CallbackFunction = std::function<json(Arguments& args)>;
291+using VoidCallbackFunction = std::function<void(Arguments& args)>;
292+
293+/*!
294+ * \brief Class for builtin functions and user-defined callbacks.
295+ */
296+class FunctionStorage {
297+public:
298+ enum class Operation {
299+ Not,
300+ And,
301+ Or,
302+ In,
303+ Equal,
304+ NotEqual,
305+ Greater,
306+ GreaterEqual,
307+ Less,
308+ LessEqual,
309+ Add,
310+ Subtract,
311+ Multiplication,
312+ Division,
313+ Power,
314+ Modulo,
315+ AtId,
316+ At,
317+ Capitalize,
318+ Default,
319+ DivisibleBy,
320+ Even,
321+ Exists,
322+ ExistsInObject,
323+ First,
324+ Float,
325+ Int,
326+ IsArray,
327+ IsBoolean,
328+ IsFloat,
329+ IsInteger,
330+ IsNumber,
331+ IsObject,
332+ IsString,
333+ Last,
334+ Length,
335+ Lower,
336+ Max,
337+ Min,
338+ Odd,
339+ Range,
340+ Replace,
341+ Round,
342+ Sort,
343+ Upper,
344+ Super,
345+ Join,
346+ Callback,
347+ None,
348+ };
349+
350+ struct FunctionData {
351+ explicit FunctionData(const Operation& op, const CallbackFunction& cb = CallbackFunction {}): operation(op), callback(cb) {}
352+ const Operation operation;
353+ const CallbackFunction callback;
354+ };
355+
356+private:
357+ const int VARIADIC {-1};
358+
359+ std::map<std::pair<std::string, int>, FunctionData> function_storage = {
360+ {std::make_pair("at", 2), FunctionData {Operation::At}},
361+ {std::make_pair("capitalize", 1), FunctionData {Operation::Capitalize}},
362+ {std::make_pair("def", 2), FunctionData {Operation::Default}},
363+ {std::make_pair("divisibleBy", 2), FunctionData {Operation::DivisibleBy}},
364+ {std::make_pair("even", 1), FunctionData {Operation::Even}},
365+ {std::make_pair("exists", 1), FunctionData {Operation::Exists}},
366+ {std::make_pair("existsIn", 2), FunctionData {Operation::ExistsInObject}},
367+ {std::make_pair("first", 1), FunctionData {Operation::First}},
368+ {std::make_pair("float", 1), FunctionData {Operation::Float}},
369+ {std::make_pair("int", 1), FunctionData {Operation::Int}},
370+ {std::make_pair("isArray", 1), FunctionData {Operation::IsArray}},
371+ {std::make_pair("isBoolean", 1), FunctionData {Operation::IsBoolean}},
372+ {std::make_pair("isFloat", 1), FunctionData {Operation::IsFloat}},
373+ {std::make_pair("isInteger", 1), FunctionData {Operation::IsInteger}},
374+ {std::make_pair("isNumber", 1), FunctionData {Operation::IsNumber}},
375+ {std::make_pair("isObject", 1), FunctionData {Operation::IsObject}},
376+ {std::make_pair("isString", 1), FunctionData {Operation::IsString}},
377+ {std::make_pair("last", 1), FunctionData {Operation::Last}},
378+ {std::make_pair("length", 1), FunctionData {Operation::Length}},
379+ {std::make_pair("lower", 1), FunctionData {Operation::Lower}},
380+ {std::make_pair("max", 1), FunctionData {Operation::Max}},
381+ {std::make_pair("min", 1), FunctionData {Operation::Min}},
382+ {std::make_pair("odd", 1), FunctionData {Operation::Odd}},
383+ {std::make_pair("range", 1), FunctionData {Operation::Range}},
384+ {std::make_pair("replace", 3), FunctionData {Operation::Replace}},
385+ {std::make_pair("round", 2), FunctionData {Operation::Round}},
386+ {std::make_pair("sort", 1), FunctionData {Operation::Sort}},
387+ {std::make_pair("upper", 1), FunctionData {Operation::Upper}},
388+ {std::make_pair("super", 0), FunctionData {Operation::Super}},
389+ {std::make_pair("super", 1), FunctionData {Operation::Super}},
390+ {std::make_pair("join", 2), FunctionData {Operation::Join}},
391+ };
392+
393+public:
394+ void add_builtin(std::string_view name, int num_args, Operation op) {
395+ function_storage.emplace(std::make_pair(static_cast<std::string>(name), num_args), FunctionData {op});
396+ }
397+
398+ void add_callback(std::string_view name, int num_args, const CallbackFunction& callback) {
399+ function_storage.emplace(std::make_pair(static_cast<std::string>(name), num_args), FunctionData {Operation::Callback, callback});
400+ }
401+
402+ FunctionData find_function(std::string_view name, int num_args) const {
403+ auto it = function_storage.find(std::make_pair(static_cast<std::string>(name), num_args));
404+ if (it != function_storage.end()) {
405+ return it->second;
406+
407+ // Find variadic function
408+ } else if (num_args > 0) {
409+ it = function_storage.find(std::make_pair(static_cast<std::string>(name), VARIADIC));
410+ if (it != function_storage.end()) {
411+ return it->second;
412+ }
413+ }
414+
415+ return FunctionData {Operation::None};
416+ }
417+};
418+
419+} // namespace finja
420+
421+#endif // INCLUDE_FINJA_FUNCTION_STORAGE_HPP_
422+
423+// #include "utils.hpp"
424+#ifndef INCLUDE_FINJA_UTILS_HPP_
425+#define INCLUDE_FINJA_UTILS_HPP_
426+
427+#include <algorithm>
428+#include <cstddef>
429+#include <string>
430+#include <string_view>
431+#include <utility>
432+
433+// #include "exceptions.hpp"
434+#ifndef INCLUDE_FINJA_EXCEPTIONS_HPP_
435+#define INCLUDE_FINJA_EXCEPTIONS_HPP_
436+
437+#include <cstddef>
438+#include <stdexcept>
439+#include <string>
440+
441+namespace finja {
442+
443+struct SourceLocation {
444+ size_t line;
445+ size_t column;
446+};
447+
448+struct InjaError : public std::runtime_error {
449+ const std::string type;
450+ const std::string message;
451+
452+ const SourceLocation location;
453+
454+ explicit InjaError(const std::string& type, const std::string& message)
455+ : std::runtime_error("[inja.exception." + type + "] " + message), type(type), message(message), location({0, 0}) {}
456+
457+ explicit InjaError(const std::string& type, const std::string& message, SourceLocation location)
458+ : std::runtime_error("[inja.exception." + type + "] (" + CURRENT_FILENAME + ":" + std::to_string(location.line) + " col: " + std::to_string(location.column) + ") " + message),
459+ type(type), message(message), location(location) {}
460+};
461+
462+struct ParserError : public InjaError {
463+ explicit ParserError(const std::string& message, SourceLocation location): InjaError("parser_error", message, location) {}
464+};
465+
466+struct RenderError : public InjaError {
467+ explicit RenderError(const std::string& message, SourceLocation location): InjaError("render_error", message, location) {}
468+};
469+
470+struct FileError : public InjaError {
471+ explicit FileError(const std::string& message): InjaError("file_error", message) {}
472+ explicit FileError(const std::string& message, SourceLocation location): InjaError("file_error", message, location) {}
473+};
474+
475+struct DataError : public InjaError {
476+ explicit DataError(const std::string& message, SourceLocation location): InjaError("data_error", message, location) {}
477+};
478+
479+} // namespace finja
480+
481+#endif // INCLUDE_FINJA_EXCEPTIONS_HPP_
482+
483+
484+namespace finja {
485+
486+namespace string_view {
487+inline std::string_view slice(std::string_view view, size_t start, size_t end) {
488+ start = std::min(start, view.size());
489+ end = std::min(std::max(start, end), view.size());
490+ return view.substr(start, end - start);
491+}
492+
493+inline std::pair<std::string_view, std::string_view> split(std::string_view view, char Separator) {
494+ const size_t idx = view.find(Separator);
495+ if (idx == std::string_view::npos) {
496+ return std::make_pair(view, std::string_view());
497+ }
498+ return std::make_pair(slice(view, 0, idx), slice(view, idx + 1, std::string_view::npos));
499+}
500+
501+inline bool starts_with(std::string_view view, std::string_view prefix) {
502+ return (view.size() >= prefix.size() && view.compare(0, prefix.size(), prefix) == 0);
503+}
504+} // namespace string_view
505+
506+inline SourceLocation get_source_location(std::string_view content, size_t pos) {
507+ // Get line and offset position (starts at 1:1)
508+ auto sliced = string_view::slice(content, 0, pos);
509+ const std::size_t last_newline = sliced.rfind('\n');
510+
511+ if (last_newline == std::string_view::npos) {
512+ return {1, sliced.length() + 1};
513+ }
514+
515+ // Count newlines
516+ size_t count_lines = 0;
517+ size_t search_start = 0;
518+ while (search_start <= sliced.size()) {
519+ search_start = sliced.find('\n', search_start) + 1;
520+ if (search_start == 0) {
521+ break;
522+ }
523+ count_lines += 1;
524+ }
525+
526+ return {count_lines + 1, sliced.length() - last_newline};
527+}
528+
529+inline void replace_substring(std::string& s, const std::string& f, const std::string& t) {
530+ if (f.empty()) {
531+ return;
532+ }
533+ for (auto pos = s.find(f); // find first occurrence of f
534+ pos != std::string::npos; // make sure f was found
535+ s.replace(pos, f.size(), t), // replace with t, and
536+ pos = s.find(f, pos + t.size())) // find next occurrence of f
537+ {}
538+}
539+
540+} // namespace finja
541+
542+#endif // INCLUDE_FINJA_UTILS_HPP_
543+
544+// #include "json.hpp"
545+
546+
547+namespace finja {
548+
549+class NodeVisitor;
550+class BlockNode;
551+class TextNode;
552+class ExpressionNode;
553+class LiteralNode;
554+class DataNode;
555+class FunctionNode;
556+class ExpressionListNode;
557+class StatementNode;
558+class ForStatementNode;
559+class ForArrayStatementNode;
560+class ForObjectStatementNode;
561+class IfStatementNode;
562+class IncludeStatementNode;
563+class ExtendsStatementNode;
564+class BlockStatementNode;
565+class SetStatementNode;
566+
567+class NodeVisitor {
568+public:
569+ virtual ~NodeVisitor() = default;
570+
571+ virtual void visit(const BlockNode& node) = 0;
572+ virtual void visit(const TextNode& node) = 0;
573+ virtual void visit(const ExpressionNode& node) = 0;
574+ virtual void visit(const LiteralNode& node) = 0;
575+ virtual void visit(const DataNode& node) = 0;
576+ virtual void visit(const FunctionNode& node) = 0;
577+ virtual void visit(const ExpressionListNode& node) = 0;
578+ virtual void visit(const StatementNode& node) = 0;
579+ virtual void visit(const ForStatementNode& node) = 0;
580+ virtual void visit(const ForArrayStatementNode& node) = 0;
581+ virtual void visit(const ForObjectStatementNode& node) = 0;
582+ virtual void visit(const IfStatementNode& node) = 0;
583+ virtual void visit(const IncludeStatementNode& node) = 0;
584+ virtual void visit(const ExtendsStatementNode& node) = 0;
585+ virtual void visit(const BlockStatementNode& node) = 0;
586+ virtual void visit(const SetStatementNode& node) = 0;
587+};
588+
589+/*!
590+ * \brief Base node class for the abstract syntax tree (AST).
591+ */
592+class AstNode {
593+public:
594+ virtual void accept(NodeVisitor& v) const = 0;
595+
596+ size_t pos;
597+
598+ explicit AstNode(size_t pos): pos(pos) {}
599+ virtual ~AstNode() {}
600+};
601+
602+class BlockNode : public AstNode {
603+public:
604+ std::vector<std::shared_ptr<AstNode>> nodes;
605+
606+ explicit BlockNode(): AstNode(0) {}
607+
608+ void accept(NodeVisitor& v) const override {
609+ v.visit(*this);
610+ }
611+};
612+
613+class TextNode : public AstNode {
614+public:
615+ const size_t length;
616+
617+ explicit TextNode(size_t pos, size_t length): AstNode(pos), length(length) {}
618+
619+ void accept(NodeVisitor& v) const override {
620+ v.visit(*this);
621+ }
622+};
623+
624+class ExpressionNode : public AstNode {
625+public:
626+ explicit ExpressionNode(size_t pos): AstNode(pos) {}
627+
628+ void accept(NodeVisitor& v) const override {
629+ v.visit(*this);
630+ }
631+};
632+
633+class LiteralNode : public ExpressionNode {
634+public:
635+ const json value;
636+
637+ explicit LiteralNode(std::string_view data_text, size_t pos): ExpressionNode(pos), value([&]() {
638+ std::string json_text(data_text);
639+ if (!json_text.empty() && json_text.front() == '\'') {
640+ json_text.front() = '"';
641+ if (json_text.back() == '\'') {
642+ json_text.back() = '"';
643+ }
644+ }
645+ return json::parse(json_text);
646+ }()) {}
647+
648+ void accept(NodeVisitor& v) const override {
649+ v.visit(*this);
650+ }
651+};
652+
653+class DataNode : public ExpressionNode {
654+public:
655+ const std::string name;
656+ const json::json_pointer ptr;
657+
658+ static std::string convert_dot_to_ptr(std::string_view ptr_name) {
659+ std::string result;
660+ do {
661+ std::string_view part;
662+ std::tie(part, ptr_name) = string_view::split(ptr_name, '.');
663+ result.push_back('/');
664+ result.append(part.begin(), part.end());
665+ } while (!ptr_name.empty());
666+ return result;
667+ }
668+
669+ explicit DataNode(std::string_view ptr_name, size_t pos): ExpressionNode(pos), name(ptr_name), ptr(json::json_pointer(convert_dot_to_ptr(ptr_name))) {}
670+
671+ void accept(NodeVisitor& v) const override {
672+ v.visit(*this);
673+ }
674+};
675+
676+class FunctionNode : public ExpressionNode {
677+ using Op = FunctionStorage::Operation;
678+
679+public:
680+ enum class Associativity {
681+ Left,
682+ Right,
683+ };
684+
685+ unsigned int precedence;
686+ Associativity associativity;
687+
688+ Op operation;
689+
690+ std::string name;
691+ int number_args; // Can also be negative -> -1 for unknown number
692+ std::vector<std::shared_ptr<ExpressionNode>> arguments;
693+ CallbackFunction callback;
694+
695+ explicit FunctionNode(std::string_view name, size_t pos)
696+ : ExpressionNode(pos), precedence(8), associativity(Associativity::Left), operation(Op::Callback), name(name), number_args(0) {}
697+ explicit FunctionNode(Op operation, size_t pos): ExpressionNode(pos), operation(operation), number_args(1) {
698+ switch (operation) {
699+ case Op::Not: {
700+ number_args = 1;
701+ precedence = 4;
702+ associativity = Associativity::Left;
703+ } break;
704+ case Op::And: {
705+ number_args = 2;
706+ precedence = 1;
707+ associativity = Associativity::Left;
708+ } break;
709+ case Op::Or: {
710+ number_args = 2;
711+ precedence = 1;
712+ associativity = Associativity::Left;
713+ } break;
714+ case Op::In: {
715+ number_args = 2;
716+ precedence = 2;
717+ associativity = Associativity::Left;
718+ } break;
719+ case Op::Equal: {
720+ number_args = 2;
721+ precedence = 2;
722+ associativity = Associativity::Left;
723+ } break;
724+ case Op::NotEqual: {
725+ number_args = 2;
726+ precedence = 2;
727+ associativity = Associativity::Left;
728+ } break;
729+ case Op::Greater: {
730+ number_args = 2;
731+ precedence = 2;
732+ associativity = Associativity::Left;
733+ } break;
734+ case Op::GreaterEqual: {
735+ number_args = 2;
736+ precedence = 2;
737+ associativity = Associativity::Left;
738+ } break;
739+ case Op::Less: {
740+ number_args = 2;
741+ precedence = 2;
742+ associativity = Associativity::Left;
743+ } break;
744+ case Op::LessEqual: {
745+ number_args = 2;
746+ precedence = 2;
747+ associativity = Associativity::Left;
748+ } break;
749+ case Op::Add: {
750+ number_args = 2;
751+ precedence = 3;
752+ associativity = Associativity::Left;
753+ } break;
754+ case Op::Subtract: {
755+ number_args = 2;
756+ precedence = 3;
757+ associativity = Associativity::Left;
758+ } break;
759+ case Op::Multiplication: {
760+ number_args = 2;
761+ precedence = 4;
762+ associativity = Associativity::Left;
763+ } break;
764+ case Op::Division: {
765+ number_args = 2;
766+ precedence = 4;
767+ associativity = Associativity::Left;
768+ } break;
769+ case Op::Power: {
770+ number_args = 2;
771+ precedence = 5;
772+ associativity = Associativity::Right;
773+ } break;
774+ case Op::Modulo: {
775+ number_args = 2;
776+ precedence = 4;
777+ associativity = Associativity::Left;
778+ } break;
779+ case Op::AtId: {
780+ number_args = 2;
781+ precedence = 8;
782+ associativity = Associativity::Left;
783+ } break;
784+ default: {
785+ precedence = 1;
786+ associativity = Associativity::Left;
787+ }
788+ }
789+ }
790+
791+ void accept(NodeVisitor& v) const override {
792+ v.visit(*this);
793+ }
794+};
795+
796+class ExpressionListNode : public AstNode {
797+public:
798+ std::shared_ptr<ExpressionNode> root;
799+
800+ explicit ExpressionListNode(): AstNode(0) {}
801+ explicit ExpressionListNode(size_t pos): AstNode(pos) {}
802+
803+ void accept(NodeVisitor& v) const override {
804+ v.visit(*this);
805+ }
806+};
807+
808+class StatementNode : public AstNode {
809+public:
810+ explicit StatementNode(size_t pos): AstNode(pos) {}
811+
812+ virtual void accept(NodeVisitor& v) const = 0;
813+};
814+
815+class ForStatementNode : public StatementNode {
816+public:
817+ ExpressionListNode condition;
818+ BlockNode body;
819+ BlockNode* const parent;
820+
821+ explicit ForStatementNode(BlockNode* const parent, size_t pos): StatementNode(pos), parent(parent) {}
822+
823+ virtual void accept(NodeVisitor& v) const = 0;
824+};
825+
826+class ForArrayStatementNode : public ForStatementNode {
827+public:
828+ const std::string value;
829+
830+ explicit ForArrayStatementNode(const std::string& value, BlockNode* const parent, size_t pos): ForStatementNode(parent, pos), value(value) {}
831+
832+ void accept(NodeVisitor& v) const override {
833+ v.visit(*this);
834+ }
835+};
836+
837+class ForObjectStatementNode : public ForStatementNode {
838+public:
839+ const std::string key;
840+ const std::string value;
841+
842+ explicit ForObjectStatementNode(const std::string& key, const std::string& value, BlockNode* const parent, size_t pos)
843+ : ForStatementNode(parent, pos), key(key), value(value) {}
844+
845+ void accept(NodeVisitor& v) const override {
846+ v.visit(*this);
847+ }
848+};
849+
850+class IfStatementNode : public StatementNode {
851+public:
852+ ExpressionListNode condition;
853+ BlockNode true_statement;
854+ BlockNode false_statement;
855+ BlockNode* const parent;
856+
857+ const bool is_nested;
858+ bool has_false_statement {false};
859+
860+ explicit IfStatementNode(BlockNode* const parent, size_t pos): StatementNode(pos), parent(parent), is_nested(false) {}
861+ explicit IfStatementNode(bool is_nested, BlockNode* const parent, size_t pos): StatementNode(pos), parent(parent), is_nested(is_nested) {}
862+
863+ void accept(NodeVisitor& v) const override {
864+ v.visit(*this);
865+ }
866+};
867+
868+class IncludeStatementNode : public StatementNode {
869+public:
870+ const std::string file;
871+
872+ explicit IncludeStatementNode(const std::string& file, size_t pos): StatementNode(pos), file(file) {}
873+
874+ void accept(NodeVisitor& v) const override {
875+ v.visit(*this);
876+ }
877+};
878+
879+class ExtendsStatementNode : public StatementNode {
880+public:
881+ const std::string file;
882+
883+ explicit ExtendsStatementNode(const std::string& file, size_t pos): StatementNode(pos), file(file) {}
884+
885+ void accept(NodeVisitor& v) const override {
886+ v.visit(*this);
887+ }
888+};
889+
890+class BlockStatementNode : public StatementNode {
891+public:
892+ const std::string name;
893+ BlockNode block;
894+ BlockNode* const parent;
895+
896+ explicit BlockStatementNode(BlockNode* const parent, const std::string& name, size_t pos): StatementNode(pos), name(name), parent(parent) {}
897+
898+ void accept(NodeVisitor& v) const override {
899+ v.visit(*this);
900+ }
901+};
902+
903+class SetStatementNode : public StatementNode {
904+public:
905+ const std::string key;
906+ ExpressionListNode expression;
907+
908+ explicit SetStatementNode(const std::string& key, size_t pos): StatementNode(pos), key(key) {}
909+
910+ void accept(NodeVisitor& v) const override {
911+ v.visit(*this);
912+ }
913+};
914+
915+} // namespace finja
916+
917+#endif // INCLUDE_FINJA_NODE_HPP_
918+
919+// #include "statistics.hpp"
920+#ifndef INCLUDE_FINJA_STATISTICS_HPP_
921+#define INCLUDE_FINJA_STATISTICS_HPP_
922+
923+// #include "node.hpp"
924+
925+
926+namespace finja {
927+
928+/*!
929+ * \brief A class for counting statistics on a Template.
930+ */
931+class StatisticsVisitor : public NodeVisitor {
932+ void visit(const BlockNode& node) override {
933+ for (const auto& n : node.nodes) {
934+ n->accept(*this);
935+ }
936+ }
937+
938+ void visit(const TextNode&) override {}
939+ void visit(const ExpressionNode&) override {}
940+ void visit(const LiteralNode&) override {}
941+
942+ void visit(const DataNode&) override {
943+ variable_counter += 1;
944+ }
945+
946+ void visit(const FunctionNode& node) override {
947+ for (const auto& n : node.arguments) {
948+ n->accept(*this);
949+ }
950+ }
951+
952+ void visit(const ExpressionListNode& node) override {
953+ node.root->accept(*this);
954+ }
955+
956+ void visit(const StatementNode&) override {}
957+ void visit(const ForStatementNode&) override {}
958+
959+ void visit(const ForArrayStatementNode& node) override {
960+ node.condition.accept(*this);
961+ node.body.accept(*this);
962+ }
963+
964+ void visit(const ForObjectStatementNode& node) override {
965+ node.condition.accept(*this);
966+ node.body.accept(*this);
967+ }
968+
969+ void visit(const IfStatementNode& node) override {
970+ node.condition.accept(*this);
971+ node.true_statement.accept(*this);
972+ node.false_statement.accept(*this);
973+ }
974+
975+ void visit(const IncludeStatementNode&) override {}
976+
977+ void visit(const ExtendsStatementNode&) override {}
978+
979+ void visit(const BlockStatementNode& node) override {
980+ node.block.accept(*this);
981+ }
982+
983+ void visit(const SetStatementNode&) override {}
984+
985+public:
986+ size_t variable_counter {0};
987+
988+ explicit StatisticsVisitor() {}
989+};
990+
991+} // namespace finja
992+
993+#endif // INCLUDE_FINJA_STATISTICS_HPP_
994+
995+
996+namespace finja {
997+
998+/*!
999+ * \brief The main inja Template.
1000+ */
1001+struct Template {
1002+ BlockNode root;
1003+ std::string content;
1004+ std::map<std::string, std::shared_ptr<BlockStatementNode>> block_storage;
1005+
1006+ explicit Template() {}
1007+ explicit Template(std::string content): content(std::move(content)) {}
1008+
1009+ /// Return number of variables (total number, not distinct ones) in the template
1010+ size_t count_variables() const {
1011+ auto statistic_visitor = StatisticsVisitor();
1012+ root.accept(statistic_visitor);
1013+ return statistic_visitor.variable_counter;
1014+ }
1015+};
1016+
1017+using TemplateStorage = std::map<std::string, Template>;
1018+
1019+} // namespace finja
1020+
1021+#endif // INCLUDE_FINJA_TEMPLATE_HPP_
1022+
1023+
1024+namespace finja {
1025+
1026+/*!
1027+ * \brief Class for lexer configuration.
1028+ */
1029+struct LexerConfig {
1030+ std::string statement_open {"{%"};
1031+ std::string statement_open_no_lstrip {"{%+"};
1032+ std::string statement_open_force_lstrip {"{%-"};
1033+ std::string statement_close {"%}"};
1034+ std::string statement_close_force_rstrip {"-%}"};
1035+ std::string line_statement {"##"};
1036+ std::string expression_open {"{{"};
1037+ std::string expression_open_force_lstrip {"{{-"};
1038+ std::string expression_close {"}}"};
1039+ std::string expression_close_force_rstrip {"-}}"};
1040+ std::string comment_open {"{#"};
1041+ std::string comment_open_force_lstrip {"{#-"};
1042+ std::string comment_close {"#}"};
1043+ std::string comment_close_force_rstrip {"-#}"};
1044+ std::string open_chars {"#{"};
1045+
1046+ bool trim_blocks {true};
1047+ bool lstrip_blocks {true};
1048+
1049+ void update_open_chars() {
1050+ open_chars = "";
1051+ if (open_chars.find(line_statement[0]) == std::string::npos) {
1052+ open_chars += line_statement[0];
1053+ }
1054+ if (open_chars.find(statement_open[0]) == std::string::npos) {
1055+ open_chars += statement_open[0];
1056+ }
1057+ if (open_chars.find(statement_open_no_lstrip[0]) == std::string::npos) {
1058+ open_chars += statement_open_no_lstrip[0];
1059+ }
1060+ if (open_chars.find(statement_open_force_lstrip[0]) == std::string::npos) {
1061+ open_chars += statement_open_force_lstrip[0];
1062+ }
1063+ if (open_chars.find(expression_open[0]) == std::string::npos) {
1064+ open_chars += expression_open[0];
1065+ }
1066+ if (open_chars.find(expression_open_force_lstrip[0]) == std::string::npos) {
1067+ open_chars += expression_open_force_lstrip[0];
1068+ }
1069+ if (open_chars.find(comment_open[0]) == std::string::npos) {
1070+ open_chars += comment_open[0];
1071+ }
1072+ if (open_chars.find(comment_open_force_lstrip[0]) == std::string::npos) {
1073+ open_chars += comment_open_force_lstrip[0];
1074+ }
1075+ }
1076+};
1077+
1078+/*!
1079+ * \brief Class for parser configuration.
1080+ */
1081+struct ParserConfig {
1082+ bool search_included_templates_in_files {true};
1083+
1084+ std::function<Template(const std::filesystem::path&, const std::string&)> include_callback;
1085+};
1086+
1087+/*!
1088+ * \brief Class for render configuration.
1089+ */
1090+struct RenderConfig {
1091+ bool throw_at_missing_includes {true};
1092+ bool html_autoescape {false};
1093+};
1094+
1095+} // namespace finja
1096+
1097+#endif // INCLUDE_FINJA_CONFIG_HPP_
1098+
1099+// #include "function_storage.hpp"
1100+
1101+// #include "parser.hpp"
1102+#ifndef INCLUDE_FINJA_PARSER_HPP_
1103+#define INCLUDE_FINJA_PARSER_HPP_
1104+
1105+#include <cstddef>
1106+#include <filesystem>
1107+#include <fstream>
1108+#include <iterator>
1109+#include <memory>
1110+#include <stack>
1111+#include <string>
1112+#include <string_view>
1113+#include <utility>
1114+#include <vector>
1115+
1116+// #include "config.hpp"
1117+
1118+// #include "exceptions.hpp"
1119+
1120+// #include "function_storage.hpp"
1121+
1122+// #include "lexer.hpp"
1123+#ifndef INCLUDE_FINJA_LEXER_HPP_
1124+#define INCLUDE_FINJA_LEXER_HPP_
1125+
1126+#include <cctype>
1127+#include <cstddef>
1128+#include <string_view>
1129+
1130+// #include "config.hpp"
1131+
1132+// #include "exceptions.hpp"
1133+
1134+// #include "token.hpp"
1135+#ifndef INCLUDE_FINJA_TOKEN_HPP_
1136+#define INCLUDE_FINJA_TOKEN_HPP_
1137+
1138+#include <string>
1139+#include <string_view>
1140+
1141+namespace finja {
1142+
1143+/*!
1144+ * \brief Helper-class for the inja Lexer.
1145+ */
1146+struct Token {
1147+ enum class Kind {
1148+ Text,
1149+ ExpressionOpen, // {{
1150+ ExpressionClose, // }}
1151+ LineStatementOpen, // ##
1152+ LineStatementClose, // \n
1153+ StatementOpen, // {%
1154+ StatementClose, // %}
1155+ CommentOpen, // {#
1156+ CommentClose, // #}
1157+ Id, // this, this.foo
1158+ Number, // 1, 2, -1, 5.2, -5.3
1159+ String, // "this"
1160+ Plus, // +
1161+ Minus, // -
1162+ Times, // *
1163+ Slash, // /
1164+ Percent, // %
1165+ Power, // ^
1166+ Comma, // ,
1167+ Dot, // .
1168+ Colon, // :
1169+ LeftParen, // (
1170+ RightParen, // )
1171+ LeftBracket, // [
1172+ RightBracket, // ]
1173+ LeftBrace, // {
1174+ RightBrace, // }
1175+ Equal, // ==
1176+ NotEqual, // !=
1177+ GreaterThan, // >
1178+ GreaterEqual, // >=
1179+ LessThan, // <
1180+ LessEqual, // <=
1181+ Pipe, // |
1182+ Unknown,
1183+ Eof,
1184+ };
1185+
1186+ Kind kind {Kind::Unknown};
1187+ std::string_view text;
1188+
1189+ explicit constexpr Token() = default;
1190+ explicit constexpr Token(Kind kind, std::string_view text): kind(kind), text(text) {}
1191+
1192+ std::string describe() const {
1193+ switch (kind) {
1194+ case Kind::Text:
1195+ return "<text>";
1196+ case Kind::LineStatementClose:
1197+ return "<eol>";
1198+ case Kind::Eof:
1199+ return "<eof>";
1200+ default:
1201+ return static_cast<std::string>(text);
1202+ }
1203+ }
1204+};
1205+
1206+} // namespace finja
1207+
1208+#endif // INCLUDE_FINJA_TOKEN_HPP_
1209+
1210+// #include "utils.hpp"
1211+
1212+
1213+namespace finja {
1214+
1215+/*!
1216+ * \brief Class for lexing an inja Template.
1217+ */
1218+class Lexer {
1219+ enum class State {
1220+ Text,
1221+ ExpressionStart,
1222+ ExpressionStartForceLstrip,
1223+ ExpressionBody,
1224+ LineStart,
1225+ LineBody,
1226+ StatementStart,
1227+ StatementStartNoLstrip,
1228+ StatementStartForceLstrip,
1229+ StatementBody,
1230+ CommentStart,
1231+ CommentStartForceLstrip,
1232+ CommentBody,
1233+ };
1234+
1235+ enum class MinusState {
1236+ Operator,
1237+ Number,
1238+ };
1239+
1240+ const LexerConfig& config;
1241+
1242+ State state;
1243+ MinusState minus_state;
1244+ bool trim_expression {false};
1245+ std::string_view m_in;
1246+ size_t tok_start;
1247+ size_t pos;
1248+
1249+ Token scan_body(std::string_view close, Token::Kind closeKind, std::string_view close_trim = std::string_view(), bool trim = false) {
1250+ again:
1251+ // skip whitespace (except for \n as it might be a close)
1252+ if (tok_start >= m_in.size()) {
1253+ return make_token(Token::Kind::Eof);
1254+ }
1255+ const char ch = m_in[tok_start];
1256+ if (ch == ' ' || ch == '\t' || ch == '\r') {
1257+ tok_start += 1;
1258+ goto again;
1259+ }
1260+
1261+ // check for close
1262+ if (!close_trim.empty() && finja::string_view::starts_with(m_in.substr(tok_start), close_trim)) {
1263+ state = State::Text;
1264+ pos = tok_start + close_trim.size();
1265+ const Token tok = make_token(closeKind);
1266+ trim_expression = false;
1267+ skip_whitespaces_and_newlines();
1268+ return tok;
1269+ }
1270+
1271+ if (finja::string_view::starts_with(m_in.substr(tok_start), close)) {
1272+ state = State::Text;
1273+ pos = tok_start + close.size();
1274+ const Token tok = make_token(closeKind);
1275+ trim_expression = false;
1276+ if (trim) {
1277+ skip_whitespaces_and_first_newline();
1278+ }
1279+ return tok;
1280+ }
1281+
1282+ // skip \n
1283+ if (ch == '\n') {
1284+ tok_start += 1;
1285+ goto again;
1286+ }
1287+
1288+ pos = tok_start + 1;
1289+ if (std::isalpha(ch)) {
1290+ minus_state = MinusState::Operator;
1291+ return scan_id();
1292+ }
1293+
1294+ const MinusState current_minus_state = minus_state;
1295+ if (minus_state == MinusState::Operator) {
1296+ minus_state = MinusState::Number;
1297+ }
1298+
1299+ switch (ch) {
1300+ case '+':
1301+ return make_token(Token::Kind::Plus);
1302+ case '-':
1303+ if (current_minus_state == MinusState::Operator) {
1304+ return make_token(Token::Kind::Minus);
1305+ }
1306+ return scan_number();
1307+ case '*':
1308+ return make_token(Token::Kind::Times);
1309+ case '/':
1310+ return make_token(Token::Kind::Slash);
1311+ case '^':
1312+ return make_token(Token::Kind::Power);
1313+ case '%':
1314+ return make_token(Token::Kind::Percent);
1315+ case '.':
1316+ return make_token(Token::Kind::Dot);
1317+ case ',':
1318+ return make_token(Token::Kind::Comma);
1319+ case ':':
1320+ return make_token(Token::Kind::Colon);
1321+ case '|':
1322+ return make_token(Token::Kind::Pipe);
1323+ case '(':
1324+ return make_token(Token::Kind::LeftParen);
1325+ case ')':
1326+ minus_state = MinusState::Operator;
1327+ return make_token(Token::Kind::RightParen);
1328+ case '[':
1329+ return make_token(Token::Kind::LeftBracket);
1330+ case ']':
1331+ minus_state = MinusState::Operator;
1332+ return make_token(Token::Kind::RightBracket);
1333+ case '{':
1334+ return make_token(Token::Kind::LeftBrace);
1335+ case '}':
1336+ minus_state = MinusState::Operator;
1337+ return make_token(Token::Kind::RightBrace);
1338+ case '>':
1339+ if (pos < m_in.size() && m_in[pos] == '=') {
1340+ pos += 1;
1341+ return make_token(Token::Kind::GreaterEqual);
1342+ }
1343+ return make_token(Token::Kind::GreaterThan);
1344+ case '<':
1345+ if (pos < m_in.size() && m_in[pos] == '=') {
1346+ pos += 1;
1347+ return make_token(Token::Kind::LessEqual);
1348+ }
1349+ return make_token(Token::Kind::LessThan);
1350+ case '=':
1351+ if (pos < m_in.size() && m_in[pos] == '=') {
1352+ pos += 1;
1353+ return make_token(Token::Kind::Equal);
1354+ }
1355+ return make_token(Token::Kind::Unknown);
1356+ case '!':
1357+ if (pos < m_in.size() && m_in[pos] == '=') {
1358+ pos += 1;
1359+ return make_token(Token::Kind::NotEqual);
1360+ }
1361+ return make_token(Token::Kind::Unknown);
1362+ case '\"':
1363+ return scan_string();
1364+ case '\'':
1365+ return scan_string();
1366+ case '0':
1367+ case '1':
1368+ case '2':
1369+ case '3':
1370+ case '4':
1371+ case '5':
1372+ case '6':
1373+ case '7':
1374+ case '8':
1375+ case '9':
1376+ minus_state = MinusState::Operator;
1377+ return scan_number();
1378+ case '_':
1379+ case '@':
1380+ case '$':
1381+ minus_state = MinusState::Operator;
1382+ return scan_id();
1383+ default:
1384+ return make_token(Token::Kind::Unknown);
1385+ }
1386+ }
1387+
1388+ Token scan_id() {
1389+ for (;;) {
1390+ if (pos >= m_in.size()) {
1391+ break;
1392+ }
1393+ const char ch = m_in[pos];
1394+ if (!std::isalnum(ch) && ch != '.' && ch != '/' && ch != '_' && ch != '-') {
1395+ break;
1396+ }
1397+ pos += 1;
1398+ }
1399+ return make_token(Token::Kind::Id);
1400+ }
1401+
1402+ Token scan_number() {
1403+ for (;;) {
1404+ if (pos >= m_in.size()) {
1405+ break;
1406+ }
1407+ const char ch = m_in[pos];
1408+ // be very permissive in lexer (we'll catch errors when conversion happens)
1409+ if (!(std::isdigit(ch) || ch == '.' || ch == 'e' || ch == 'E' || (ch == '+' && (pos == 0 || m_in[pos-1] == 'e' || m_in[pos-1] == 'E')) || (ch == '-' && (pos == 0 || m_in[pos-1] == 'e' || m_in[pos-1] == 'E')))) {
1410+ break;
1411+ }
1412+ pos += 1;
1413+ }
1414+ return make_token(Token::Kind::Number);
1415+ }
1416+
1417+ Token scan_string() {
1418+ bool escape {false};
1419+ for (;;) {
1420+ if (pos >= m_in.size()) {
1421+ break;
1422+ }
1423+ const char ch = m_in[pos++];
1424+ if (ch == '\\') {
1425+ escape = !escape;
1426+ } else if (!escape && ch == m_in[tok_start]) {
1427+ break;
1428+ } else {
1429+ escape = false;
1430+ }
1431+ }
1432+ return make_token(Token::Kind::String);
1433+ }
1434+
1435+ Token make_token(Token::Kind kind) const {
1436+ return Token(kind, string_view::slice(m_in, tok_start, pos));
1437+ }
1438+
1439+ void skip_whitespaces_and_newlines() {
1440+ if (pos < m_in.size()) {
1441+ while (pos < m_in.size() && (m_in[pos] == ' ' || m_in[pos] == '\t' || m_in[pos] == '\n' || m_in[pos] == '\r')) {
1442+ pos += 1;
1443+ }
1444+ }
1445+ }
1446+
1447+ void skip_whitespaces_and_first_newline() {
1448+ if (pos < m_in.size()) {
1449+ while (pos < m_in.size() && (m_in[pos] == ' ' || m_in[pos] == '\t')) {
1450+ pos += 1;
1451+ }
1452+ }
1453+
1454+ if (pos < m_in.size()) {
1455+ const char ch = m_in[pos];
1456+ if (ch == '\n') {
1457+ pos += 1;
1458+ } else if (ch == '\r') {
1459+ pos += 1;
1460+ if (pos < m_in.size() && m_in[pos] == '\n') {
1461+ pos += 1;
1462+ }
1463+ }
1464+ }
1465+ }
1466+
1467+ static std::string_view clear_final_line_if_whitespace(std::string_view text) {
1468+ std::string_view result = text;
1469+ while (!result.empty()) {
1470+ const char ch = result.back();
1471+ if (ch == ' ' || ch == '\t') {
1472+ result.remove_suffix(1);
1473+ } else if (ch == '\n' || ch == '\r') {
1474+ break;
1475+ } else {
1476+ return text;
1477+ }
1478+ }
1479+ return result;
1480+ }
1481+
1482+ static std::string_view trim_trailing_whitespace(std::string_view text) {
1483+ std::string_view result = text;
1484+ while (!result.empty() && (result.back() == ' ' || result.back() == '\t')) {
1485+ result.remove_suffix(1);
1486+ }
1487+ return result;
1488+ }
1489+
1490+public:
1491+ explicit Lexer(const LexerConfig& config): config(config), state(State::Text), minus_state(MinusState::Number), trim_expression(false), tok_start(0), pos(0) {}
1492+
1493+ SourceLocation current_position() const {
1494+ return get_source_location(m_in, tok_start);
1495+ }
1496+
1497+ void start(std::string_view input) {
1498+ m_in = input;
1499+ tok_start = 0;
1500+ pos = 0;
1501+ state = State::Text;
1502+ minus_state = MinusState::Number;
1503+
1504+ // Consume byte order mark (BOM) for UTF-8
1505+ if (finja::string_view::starts_with(m_in, "\xEF\xBB\xBF")) {
1506+ m_in = m_in.substr(3);
1507+ }
1508+ }
1509+
1510+ Token scan() {
1511+ tok_start = pos;
1512+
1513+ again:
1514+ if (tok_start >= m_in.size()) {
1515+ return make_token(Token::Kind::Eof);
1516+ }
1517+
1518+ switch (state) {
1519+ default:
1520+ case State::Text: {
1521+ // fast-scan to first open character
1522+ const size_t open_start = m_in.substr(pos).find_first_of(config.open_chars);
1523+ if (open_start == std::string_view::npos) {
1524+ // didn't find open, return remaining text as text token
1525+ pos = m_in.size();
1526+ return make_token(Token::Kind::Text);
1527+ }
1528+ pos += open_start;
1529+
1530+ // try to match one of the opening sequences, and get the close
1531+ const std::string_view open_str = m_in.substr(pos);
1532+ auto is_at_block_start = [&](){
1533+ size_t i = pos;
1534+ while (i > 0 && (m_in[i - 1] == ' ' || m_in[i - 1] == '\t' || m_in[i - 1] == '\r')) {
1535+ i -= 1;
1536+ }
1537+ return i == 0 || m_in[i - 1] == '\n';
1538+ };
1539+ bool must_lstrip = false;
1540+ if (finja::string_view::starts_with(open_str, config.expression_open)) {
1541+ const bool block_start = is_at_block_start();
1542+ if (finja::string_view::starts_with(open_str, config.expression_open_force_lstrip)) {
1543+ state = State::ExpressionStartForceLstrip;
1544+ must_lstrip = true;
1545+ trim_expression = config.trim_blocks && block_start;
1546+ } else {
1547+ state = State::ExpressionStart;
1548+ must_lstrip = config.lstrip_blocks && block_start;
1549+ trim_expression = config.trim_blocks && block_start;
1550+ }
1551+ } else if (finja::string_view::starts_with(open_str, config.comment_open)) {
1552+ if (finja::string_view::starts_with(open_str, config.comment_open_force_lstrip)) {
1553+ state = State::CommentStartForceLstrip;
1554+ must_lstrip = true;
1555+ } else {
1556+ state = State::CommentStart;
1557+ must_lstrip = config.lstrip_blocks && is_at_block_start();
1558+ }
1559+ } else if ((pos == 0 || m_in[pos - 1] == '\n') && finja::string_view::starts_with(open_str, config.line_statement)) {
1560+ state = State::LineStart;
1561+ } else {
1562+ pos += 1; // wasn't actually an opening sequence
1563+ goto again;
1564+ }
1565+
1566+ std::string_view text = string_view::slice(m_in, tok_start, pos);
1567+ if (must_lstrip) {
1568+ text = trim_trailing_whitespace(text);
1569+ }
1570+
1571+ if (text.empty()) {
1572+ goto again; // don't generate empty token
1573+ }
1574+ return Token(Token::Kind::Text, text);
1575+ }
1576+ case State::ExpressionStart: {
1577+ state = State::ExpressionBody;
1578+ pos += config.expression_open.size();
1579+ return make_token(Token::Kind::ExpressionOpen);
1580+ }
1581+ case State::ExpressionStartForceLstrip: {
1582+ state = State::ExpressionBody;
1583+ pos += config.expression_open_force_lstrip.size();
1584+ return make_token(Token::Kind::ExpressionOpen);
1585+ }
1586+ case State::LineStart: {
1587+ state = State::LineBody;
1588+ pos += config.line_statement.size();
1589+ return make_token(Token::Kind::LineStatementOpen);
1590+ }
1591+ case State::CommentStart: {
1592+ state = State::CommentBody;
1593+ pos += config.comment_open.size();
1594+ return make_token(Token::Kind::CommentOpen);
1595+ }
1596+ case State::CommentStartForceLstrip: {
1597+ state = State::CommentBody;
1598+ pos += config.comment_open_force_lstrip.size();
1599+ return make_token(Token::Kind::CommentOpen);
1600+ }
1601+ case State::ExpressionBody: {
1602+ const bool trim = trim_expression;
1603+ return scan_body(config.expression_close, Token::Kind::ExpressionClose, config.expression_close_force_rstrip, trim);
1604+ }
1605+ case State::LineBody:
1606+ return scan_body("\n", Token::Kind::LineStatementClose);
1607+ case State::CommentBody: {
1608+ // fast-scan to comment close
1609+ const size_t end = m_in.substr(pos).find(config.comment_close);
1610+ if (end == std::string_view::npos) {
1611+ pos = m_in.size();
1612+ return make_token(Token::Kind::Eof);
1613+ }
1614+
1615+ // Check for trim pattern
1616+ const bool must_rstrip = finja::string_view::starts_with(m_in.substr(pos + end - 1), config.comment_close_force_rstrip);
1617+
1618+ // return the entire comment in the close token
1619+ state = State::Text;
1620+ pos += end + config.comment_close.size();
1621+ Token tok = make_token(Token::Kind::CommentClose);
1622+
1623+ if (must_rstrip || config.trim_blocks) {
1624+ skip_whitespaces_and_first_newline();
1625+ }
1626+ return tok;
1627+ }
1628+ }
1629+ }
1630+
1631+ const LexerConfig& get_config() const {
1632+ return config;
1633+ }
1634+};
1635+
1636+} // namespace finja
1637+
1638+#endif // INCLUDE_FINJA_LEXER_HPP_
1639+
1640+// #include "node.hpp"
1641+
1642+// #include "template.hpp"
1643+
1644+// #include "throw.hpp"
1645+
1646+// #include "token.hpp"
1647+
1648+
1649+namespace finja {
1650+
1651+/*!
1652+ * \brief Class for parsing an inja Template.
1653+ */
1654+class Parser {
1655+ using Arguments = std::vector<std::shared_ptr<ExpressionNode>>;
1656+ using OperatorStack = std::stack<std::shared_ptr<FunctionNode>>;
1657+
1658+ const ParserConfig& config;
1659+
1660+ Lexer lexer;
1661+ TemplateStorage& template_storage;
1662+ const FunctionStorage& function_storage;
1663+
1664+ Token tok, peek_tok;
1665+ bool have_peek_tok {false};
1666+
1667+ std::string_view literal_start;
1668+
1669+ BlockNode* current_block {nullptr};
1670+ ExpressionListNode* current_expression_list {nullptr};
1671+
1672+ std::stack<IfStatementNode*> if_statement_stack;
1673+ std::stack<ForStatementNode*> for_statement_stack;
1674+ std::stack<BlockStatementNode*> block_statement_stack;
1675+
1676+ void throw_parser_error(const std::string& message) const {
1677+ INJA_THROW(ParserError(message, lexer.current_position()));
1678+ }
1679+
1680+ void get_next_token() {
1681+ if (have_peek_tok) {
1682+ tok = peek_tok;
1683+ have_peek_tok = false;
1684+ } else {
1685+ tok = lexer.scan();
1686+ }
1687+ }
1688+
1689+ void get_peek_token() {
1690+ if (!have_peek_tok) {
1691+ peek_tok = lexer.scan();
1692+ have_peek_tok = true;
1693+ }
1694+ }
1695+
1696+ void add_literal(Arguments &arguments, const char* content_ptr) {
1697+ const std::string_view data_text(literal_start.data(), tok.text.data() - literal_start.data() + tok.text.size());
1698+ arguments.emplace_back(std::make_shared<LiteralNode>(data_text, data_text.data() - content_ptr));
1699+ }
1700+
1701+ void add_operator(Arguments &arguments, OperatorStack &operator_stack) {
1702+ auto function = operator_stack.top();
1703+ operator_stack.pop();
1704+
1705+ if (static_cast<int>(arguments.size()) < function->number_args) {
1706+ throw_parser_error("too few arguments");
1707+ }
1708+
1709+ for (int i = 0; i < function->number_args; ++i) {
1710+ function->arguments.insert(function->arguments.begin(), arguments.back());
1711+ arguments.pop_back();
1712+ }
1713+ arguments.emplace_back(function);
1714+ }
1715+
1716+ void add_to_template_storage(const std::filesystem::path& path, std::string& template_name) {
1717+ if (template_storage.find(template_name) != template_storage.end()) {
1718+ return;
1719+ }
1720+
1721+ const std::string original_name = template_name;
1722+
1723+ if (config.search_included_templates_in_files) {
1724+ // Build the relative path
1725+ template_name = (path / original_name).string();
1726+ if (template_name.compare(0, 2, "./") == 0) {
1727+ template_name.erase(0, 2);
1728+ }
1729+
1730+ if (template_storage.find(template_name) == template_storage.end()) {
1731+ // Load file
1732+ std::ifstream file;
1733+ file.open(template_name);
1734+ if (!file.fail()) {
1735+ const std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
1736+
1737+ auto include_template = Template(text);
1738+ template_storage.emplace(template_name, include_template);
1739+ parse_into_template(template_storage[template_name], template_name);
1740+ return;
1741+ } else if (!config.include_callback) {
1742+ INJA_THROW(FileError("failed accessing file at '" + template_name + "'"));
1743+ }
1744+ }
1745+ }
1746+
1747+ // Try include callback
1748+ if (config.include_callback) {
1749+ auto include_template = config.include_callback(path, original_name);
1750+ template_storage.emplace(template_name, include_template);
1751+ }
1752+ }
1753+
1754+ std::string parse_filename() const {
1755+ if (tok.kind != Token::Kind::String) {
1756+ throw_parser_error("expected string, got '" + tok.describe() + "'");
1757+ }
1758+
1759+ if (tok.text.length() < 2) {
1760+ throw_parser_error("expected filename, got '" + static_cast<std::string>(tok.text) + "'");
1761+ }
1762+
1763+ // Remove first and last character ""
1764+ return std::string {tok.text.substr(1, tok.text.length() - 2)};
1765+ }
1766+
1767+ bool parse_expression(Template& tmpl, Token::Kind closing) {
1768+ current_expression_list->root = parse_expression(tmpl);
1769+ return tok.kind == closing;
1770+ }
1771+
1772+ std::shared_ptr<ExpressionNode> parse_expression(Template& tmpl) {
1773+ size_t current_bracket_level {0};
1774+ size_t current_brace_level {0};
1775+ Arguments arguments;
1776+ OperatorStack operator_stack;
1777+
1778+ while (tok.kind != Token::Kind::Eof) {
1779+ // Literals
1780+ switch (tok.kind) {
1781+ case Token::Kind::String: {
1782+ if (current_brace_level == 0 && current_bracket_level == 0) {
1783+ literal_start = tok.text;
1784+ add_literal(arguments, tmpl.content.c_str());
1785+ }
1786+ } break;
1787+ case Token::Kind::Number: {
1788+ if (current_brace_level == 0 && current_bracket_level == 0) {
1789+ literal_start = tok.text;
1790+ add_literal(arguments, tmpl.content.c_str());
1791+ }
1792+ } break;
1793+ case Token::Kind::LeftBracket: {
1794+ if (current_brace_level == 0 && current_bracket_level == 0) {
1795+ literal_start = tok.text;
1796+ }
1797+ current_bracket_level += 1;
1798+ } break;
1799+ case Token::Kind::LeftBrace: {
1800+ if (current_brace_level == 0 && current_bracket_level == 0) {
1801+ literal_start = tok.text;
1802+ }
1803+ current_brace_level += 1;
1804+ } break;
1805+ case Token::Kind::RightBracket: {
1806+ if (current_bracket_level == 0) {
1807+ throw_parser_error("unexpected ']'");
1808+ }
1809+
1810+ current_bracket_level -= 1;
1811+ if (current_brace_level == 0 && current_bracket_level == 0) {
1812+ add_literal(arguments, tmpl.content.c_str());
1813+ }
1814+ } break;
1815+ case Token::Kind::RightBrace: {
1816+ if (current_brace_level == 0) {
1817+ throw_parser_error("unexpected '}'");
1818+ }
1819+
1820+ current_brace_level -= 1;
1821+ if (current_brace_level == 0 && current_bracket_level == 0) {
1822+ add_literal(arguments, tmpl.content.c_str());
1823+ }
1824+ } break;
1825+ case Token::Kind::Id: {
1826+ get_peek_token();
1827+
1828+ // Data Literal
1829+ if (tok.text == static_cast<decltype(tok.text)>("true") || tok.text == static_cast<decltype(tok.text)>("false") ||
1830+ tok.text == static_cast<decltype(tok.text)>("null")) {
1831+ if (current_brace_level == 0 && current_bracket_level == 0) {
1832+ literal_start = tok.text;
1833+ add_literal(arguments, tmpl.content.c_str());
1834+ }
1835+
1836+ // Operator
1837+ } else if (tok.text == "and" || tok.text == "or" || tok.text == "in" || tok.text == "not") {
1838+ goto parse_operator;
1839+
1840+ // Functions
1841+ } else if (peek_tok.kind == Token::Kind::LeftParen) {
1842+ auto func = std::make_shared<FunctionNode>(tok.text, tok.text.data() - tmpl.content.c_str());
1843+ get_next_token();
1844+ do {
1845+ get_next_token();
1846+ auto expr = parse_expression(tmpl);
1847+ if (!expr) {
1848+ break;
1849+ }
1850+ func->number_args += 1;
1851+ func->arguments.emplace_back(expr);
1852+ } while (tok.kind == Token::Kind::Comma);
1853+ if (tok.kind != Token::Kind::RightParen) {
1854+ throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'");
1855+ }
1856+
1857+ auto function_data = function_storage.find_function(func->name, func->number_args);
1858+ if (function_data.operation == FunctionStorage::Operation::None) {
1859+ throw_parser_error("unknown function " + func->name);
1860+ }
1861+ func->operation = function_data.operation;
1862+ if (function_data.operation == FunctionStorage::Operation::Callback) {
1863+ func->callback = function_data.callback;
1864+ }
1865+ arguments.emplace_back(func);
1866+
1867+ // Variables
1868+ } else {
1869+ arguments.emplace_back(std::make_shared<DataNode>(static_cast<std::string>(tok.text), tok.text.data() - tmpl.content.c_str()));
1870+ }
1871+
1872+ // Operators
1873+ } break;
1874+ case Token::Kind::Equal:
1875+ case Token::Kind::NotEqual:
1876+ case Token::Kind::GreaterThan:
1877+ case Token::Kind::GreaterEqual:
1878+ case Token::Kind::LessThan:
1879+ case Token::Kind::LessEqual:
1880+ case Token::Kind::Plus:
1881+ case Token::Kind::Minus:
1882+ case Token::Kind::Times:
1883+ case Token::Kind::Slash:
1884+ case Token::Kind::Power:
1885+ case Token::Kind::Percent:
1886+ case Token::Kind::Dot: {
1887+
1888+ parse_operator:
1889+ FunctionStorage::Operation operation;
1890+ switch (tok.kind) {
1891+ case Token::Kind::Id: {
1892+ if (tok.text == "and") {
1893+ operation = FunctionStorage::Operation::And;
1894+ } else if (tok.text == "or") {
1895+ operation = FunctionStorage::Operation::Or;
1896+ } else if (tok.text == "in") {
1897+ operation = FunctionStorage::Operation::In;
1898+ } else if (tok.text == "not") {
1899+ operation = FunctionStorage::Operation::Not;
1900+ } else {
1901+ throw_parser_error("unknown operator in parser.");
1902+ }
1903+ } break;
1904+ case Token::Kind::Equal: {
1905+ operation = FunctionStorage::Operation::Equal;
1906+ } break;
1907+ case Token::Kind::NotEqual: {
1908+ operation = FunctionStorage::Operation::NotEqual;
1909+ } break;
1910+ case Token::Kind::GreaterThan: {
1911+ operation = FunctionStorage::Operation::Greater;
1912+ } break;
1913+ case Token::Kind::GreaterEqual: {
1914+ operation = FunctionStorage::Operation::GreaterEqual;
1915+ } break;
1916+ case Token::Kind::LessThan: {
1917+ operation = FunctionStorage::Operation::Less;
1918+ } break;
1919+ case Token::Kind::LessEqual: {
1920+ operation = FunctionStorage::Operation::LessEqual;
1921+ } break;
1922+ case Token::Kind::Plus: {
1923+ operation = FunctionStorage::Operation::Add;
1924+ } break;
1925+ case Token::Kind::Minus: {
1926+ operation = FunctionStorage::Operation::Subtract;
1927+ } break;
1928+ case Token::Kind::Times: {
1929+ operation = FunctionStorage::Operation::Multiplication;
1930+ } break;
1931+ case Token::Kind::Slash: {
1932+ operation = FunctionStorage::Operation::Division;
1933+ } break;
1934+ case Token::Kind::Power: {
1935+ operation = FunctionStorage::Operation::Power;
1936+ } break;
1937+ case Token::Kind::Percent: {
1938+ operation = FunctionStorage::Operation::Modulo;
1939+ } break;
1940+ case Token::Kind::Dot: {
1941+ operation = FunctionStorage::Operation::AtId;
1942+ } break;
1943+ default: {
1944+ throw_parser_error("unknown operator in parser.");
1945+ }
1946+ }
1947+ auto function_node = std::make_shared<FunctionNode>(operation, tok.text.data() - tmpl.content.c_str());
1948+
1949+ while (!operator_stack.empty() &&
1950+ ((operator_stack.top()->precedence > function_node->precedence) ||
1951+ (operator_stack.top()->precedence == function_node->precedence && function_node->associativity == FunctionNode::Associativity::Left))) {
1952+ add_operator(arguments, operator_stack);
1953+ }
1954+
1955+ operator_stack.emplace(function_node);
1956+ } break;
1957+ case Token::Kind::Comma: {
1958+ if (current_brace_level == 0 && current_bracket_level == 0) {
1959+ goto break_loop;
1960+ }
1961+ } break;
1962+ case Token::Kind::Colon: {
1963+ if (current_brace_level == 0 && current_bracket_level == 0) {
1964+ throw_parser_error("unexpected ':'");
1965+ }
1966+ } break;
1967+ case Token::Kind::LeftParen: {
1968+ get_next_token();
1969+ auto expr = parse_expression(tmpl);
1970+ if (tok.kind != Token::Kind::RightParen) {
1971+ throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'");
1972+ }
1973+ if (!expr) {
1974+ throw_parser_error("empty expression in parentheses");
1975+ }
1976+ arguments.emplace_back(expr);
1977+ } break;
1978+
1979+ // parse function call pipe syntax
1980+ case Token::Kind::Pipe: {
1981+ // get function name
1982+ get_next_token();
1983+ if (tok.kind != Token::Kind::Id) {
1984+ throw_parser_error("expected function name, got '" + tok.describe() + "'");
1985+ }
1986+ auto func = std::make_shared<FunctionNode>(tok.text, tok.text.data() - tmpl.content.c_str());
1987+ // add first parameter as last value from arguments
1988+ func->number_args += 1;
1989+ func->arguments.emplace_back(arguments.back());
1990+ arguments.pop_back();
1991+ get_peek_token();
1992+ if (peek_tok.kind == Token::Kind::LeftParen) {
1993+ get_next_token();
1994+ // parse additional parameters
1995+ do {
1996+ get_next_token();
1997+ auto expr = parse_expression(tmpl);
1998+ if (!expr) {
1999+ break;
2000+ }
2001+ func->number_args += 1;
2002+ func->arguments.emplace_back(expr);
2003+ } while (tok.kind == Token::Kind::Comma);
2004+ if (tok.kind != Token::Kind::RightParen) {
2005+ throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'");
2006+ }
2007+ }
2008+ // search store for defined function with such name and number of args
2009+ auto function_data = function_storage.find_function(func->name, func->number_args);
2010+ if (function_data.operation == FunctionStorage::Operation::None) {
2011+ throw_parser_error("unknown function " + func->name);
2012+ }
2013+ func->operation = function_data.operation;
2014+ if (function_data.operation == FunctionStorage::Operation::Callback) {
2015+ func->callback = function_data.callback;
2016+ }
2017+ arguments.emplace_back(func);
2018+ } break;
2019+ default:
2020+ goto break_loop;
2021+ }
2022+
2023+ get_next_token();
2024+ }
2025+
2026+ break_loop:
2027+ while (!operator_stack.empty()) {
2028+ add_operator(arguments, operator_stack);
2029+ }
2030+
2031+ std::shared_ptr<ExpressionNode> expr;
2032+ if (arguments.size() == 1) {
2033+ expr = arguments[0];
2034+ arguments = {};
2035+ } else if (arguments.size() > 1) {
2036+ throw_parser_error("malformed expression");
2037+ }
2038+ return expr;
2039+ }
2040+
2041+ bool parse_statement(Template& tmpl, Token::Kind closing, const std::filesystem::path& path) {
2042+ if (tok.kind != Token::Kind::Id) {
2043+ return false;
2044+ }
2045+
2046+ if (tok.text == static_cast<decltype(tok.text)>("if")) {
2047+ get_next_token();
2048+
2049+ auto if_statement_node = std::make_shared<IfStatementNode>(current_block, tok.text.data() - tmpl.content.c_str());
2050+ current_block->nodes.emplace_back(if_statement_node);
2051+ if_statement_stack.emplace(if_statement_node.get());
2052+ current_block = &if_statement_node->true_statement;
2053+ current_expression_list = &if_statement_node->condition;
2054+
2055+ if (!parse_expression(tmpl, closing)) {
2056+ return false;
2057+ }
2058+ } else if (tok.text == static_cast<decltype(tok.text)>("else")) {
2059+ if (if_statement_stack.empty()) {
2060+ throw_parser_error("else without matching if");
2061+ }
2062+ auto& if_statement_data = if_statement_stack.top();
2063+ get_next_token();
2064+
2065+ if_statement_data->has_false_statement = true;
2066+ current_block = &if_statement_data->false_statement;
2067+
2068+ // Chained else if
2069+ if (tok.kind == Token::Kind::Id && tok.text == static_cast<decltype(tok.text)>("if")) {
2070+ get_next_token();
2071+
2072+ auto if_statement_node = std::make_shared<IfStatementNode>(true, current_block, tok.text.data() - tmpl.content.c_str());
2073+ current_block->nodes.emplace_back(if_statement_node);
2074+ if_statement_stack.emplace(if_statement_node.get());
2075+ current_block = &if_statement_node->true_statement;
2076+ current_expression_list = &if_statement_node->condition;
2077+
2078+ if (!parse_expression(tmpl, closing)) {
2079+ return false;
2080+ }
2081+ }
2082+ } else if (tok.text == static_cast<decltype(tok.text)>("endif")) {
2083+ if (if_statement_stack.empty()) {
2084+ throw_parser_error("endif without matching if");
2085+ }
2086+
2087+ // Nested if statements
2088+ while (if_statement_stack.top()->is_nested) {
2089+ if_statement_stack.pop();
2090+ }
2091+
2092+ auto& if_statement_data = if_statement_stack.top();
2093+ get_next_token();
2094+
2095+ current_block = if_statement_data->parent;
2096+ if_statement_stack.pop();
2097+ } else if (tok.text == static_cast<decltype(tok.text)>("block")) {
2098+ get_next_token();
2099+
2100+ if (tok.kind != Token::Kind::Id) {
2101+ throw_parser_error("expected block name, got '" + tok.describe() + "'");
2102+ }
2103+
2104+ const std::string block_name = static_cast<std::string>(tok.text);
2105+
2106+ auto block_statement_node = std::make_shared<BlockStatementNode>(current_block, block_name, tok.text.data() - tmpl.content.c_str());
2107+ current_block->nodes.emplace_back(block_statement_node);
2108+ block_statement_stack.emplace(block_statement_node.get());
2109+ current_block = &block_statement_node->block;
2110+ auto success = tmpl.block_storage.emplace(block_name, block_statement_node);
2111+ if (!success.second) {
2112+ throw_parser_error("block with the name '" + block_name + "' does already exist");
2113+ }
2114+
2115+ get_next_token();
2116+ } else if (tok.text == static_cast<decltype(tok.text)>("endblock")) {
2117+ if (block_statement_stack.empty()) {
2118+ throw_parser_error("endblock without matching block");
2119+ }
2120+
2121+ auto& block_statement_data = block_statement_stack.top();
2122+ get_next_token();
2123+
2124+ current_block = block_statement_data->parent;
2125+ block_statement_stack.pop();
2126+ } else if (tok.text == static_cast<decltype(tok.text)>("for")) {
2127+ get_next_token();
2128+
2129+ // options: for a in arr; for a, b in obj
2130+ if (tok.kind != Token::Kind::Id) {
2131+ throw_parser_error("expected id, got '" + tok.describe() + "'");
2132+ }
2133+
2134+ Token value_token = tok;
2135+ get_next_token();
2136+
2137+ // Object type
2138+ std::shared_ptr<ForStatementNode> for_statement_node;
2139+ if (tok.kind == Token::Kind::Comma) {
2140+ get_next_token();
2141+ if (tok.kind != Token::Kind::Id) {
2142+ throw_parser_error("expected id, got '" + tok.describe() + "'");
2143+ }
2144+
2145+ const Token key_token = value_token;
2146+ value_token = tok;
2147+ get_next_token();
2148+
2149+ for_statement_node = std::make_shared<ForObjectStatementNode>(static_cast<std::string>(key_token.text), static_cast<std::string>(value_token.text),
2150+ current_block, tok.text.data() - tmpl.content.c_str());
2151+
2152+ // Array type
2153+ } else {
2154+ for_statement_node =
2155+ std::make_shared<ForArrayStatementNode>(static_cast<std::string>(value_token.text), current_block, tok.text.data() - tmpl.content.c_str());
2156+ }
2157+
2158+ current_block->nodes.emplace_back(for_statement_node);
2159+ for_statement_stack.emplace(for_statement_node.get());
2160+ current_block = &for_statement_node->body;
2161+ current_expression_list = &for_statement_node->condition;
2162+
2163+ if (tok.kind != Token::Kind::Id || tok.text != static_cast<decltype(tok.text)>("in")) {
2164+ throw_parser_error("expected 'in', got '" + tok.describe() + "'");
2165+ }
2166+ get_next_token();
2167+
2168+ if (!parse_expression(tmpl, closing)) {
2169+ return false;
2170+ }
2171+ } else if (tok.text == static_cast<decltype(tok.text)>("endfor")) {
2172+ if (for_statement_stack.empty()) {
2173+ throw_parser_error("endfor without matching for");
2174+ }
2175+
2176+ auto& for_statement_data = for_statement_stack.top();
2177+ get_next_token();
2178+
2179+ current_block = for_statement_data->parent;
2180+ for_statement_stack.pop();
2181+ } else if (tok.text == static_cast<decltype(tok.text)>("include")) {
2182+ get_next_token();
2183+
2184+ std::string template_name = parse_filename();
2185+ add_to_template_storage(path, template_name);
2186+
2187+ current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
2188+
2189+ get_next_token();
2190+ } else if (tok.text == static_cast<decltype(tok.text)>("extends")) {
2191+ get_next_token();
2192+
2193+ std::string template_name = parse_filename();
2194+ add_to_template_storage(path, template_name);
2195+
2196+ current_block->nodes.emplace_back(std::make_shared<ExtendsStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
2197+
2198+ get_next_token();
2199+ } else if (tok.text == static_cast<decltype(tok.text)>("set")) {
2200+ get_next_token();
2201+
2202+ if (tok.kind != Token::Kind::Id) {
2203+ throw_parser_error("expected variable name, got '" + tok.describe() + "'");
2204+ }
2205+
2206+ const std::string key = static_cast<std::string>(tok.text);
2207+ get_next_token();
2208+
2209+ auto set_statement_node = std::make_shared<SetStatementNode>(key, tok.text.data() - tmpl.content.c_str());
2210+ current_block->nodes.emplace_back(set_statement_node);
2211+ current_expression_list = &set_statement_node->expression;
2212+
2213+ if (tok.text != static_cast<decltype(tok.text)>("=")) {
2214+ throw_parser_error("expected '=', got '" + tok.describe() + "'");
2215+ }
2216+ get_next_token();
2217+
2218+ if (!parse_expression(tmpl, closing)) {
2219+ return false;
2220+ }
2221+ } else {
2222+ return false;
2223+ }
2224+ return true;
2225+ }
2226+
2227+ void parse_into(Template& tmpl, const std::filesystem::path& path) {
2228+ lexer.start(tmpl.content);
2229+ current_block = &tmpl.root;
2230+
2231+ for (;;) {
2232+ get_next_token();
2233+ switch (tok.kind) {
2234+ case Token::Kind::Eof: {
2235+ if (!if_statement_stack.empty()) {
2236+ throw_parser_error("unmatched if");
2237+ }
2238+ if (!for_statement_stack.empty()) {
2239+ throw_parser_error("unmatched for");
2240+ }
2241+ }
2242+ current_block = nullptr;
2243+ return;
2244+ case Token::Kind::Text: {
2245+ current_block->nodes.emplace_back(std::make_shared<TextNode>(tok.text.data() - tmpl.content.c_str(), tok.text.size()));
2246+ } break;
2247+ case Token::Kind::StatementOpen: {
2248+ get_next_token();
2249+ if (!parse_statement(tmpl, Token::Kind::StatementClose, path)) {
2250+ std::string error_msg = "expected statement, got '" + tok.describe() + "'";
2251+ if (tok.text == static_cast<decltype(tok.text)>("end")) {
2252+ error_msg += ". Did you mean 'endif', 'endfor', or 'endblock'?";
2253+ }
2254+ throw_parser_error(error_msg);
2255+ }
2256+ if (tok.kind != Token::Kind::StatementClose) {
2257+ throw_parser_error("expected statement close, got '" + tok.describe() + "'");
2258+ }
2259+ } break;
2260+ case Token::Kind::LineStatementOpen: {
2261+ get_next_token();
2262+ if (!parse_statement(tmpl, Token::Kind::LineStatementClose, path)) {
2263+ throw_parser_error("expected statement, got '" + tok.describe() + "'");
2264+ }
2265+ if (tok.kind != Token::Kind::LineStatementClose && tok.kind != Token::Kind::Eof) {
2266+ throw_parser_error("expected line statement close, got '" + tok.describe() + "'");
2267+ }
2268+ } break;
2269+ case Token::Kind::ExpressionOpen: {
2270+ get_next_token();
2271+
2272+ const bool is_statement = tok.kind == Token::Kind::Id && (
2273+ tok.text == static_cast<decltype(tok.text)>("if") ||
2274+ tok.text == static_cast<decltype(tok.text)>("else") ||
2275+ tok.text == static_cast<decltype(tok.text)>("endif") ||
2276+ tok.text == static_cast<decltype(tok.text)>("for") ||
2277+ tok.text == static_cast<decltype(tok.text)>("endfor") ||
2278+ tok.text == static_cast<decltype(tok.text)>("block") ||
2279+ tok.text == static_cast<decltype(tok.text)>("endblock") ||
2280+ tok.text == static_cast<decltype(tok.text)>("include") ||
2281+ tok.text == static_cast<decltype(tok.text)>("extends") ||
2282+ tok.text == static_cast<decltype(tok.text)>("set"));
2283+
2284+ if (is_statement) {
2285+ if (!parse_statement(tmpl, Token::Kind::ExpressionClose, path)) {
2286+ std::string error_msg = "expected statement, got '" + tok.describe() + "'";
2287+ if (tok.text == static_cast<decltype(tok.text)>("end")) {
2288+ error_msg += ". Did you mean 'endif', 'endfor', or 'endblock'?";
2289+ }
2290+ throw_parser_error(error_msg);
2291+ }
2292+ if (tok.kind != Token::Kind::ExpressionClose) {
2293+ throw_parser_error("expected expression close, got '" + tok.describe() + "'");
2294+ }
2295+ } else {
2296+ auto expression_list_node = std::make_shared<ExpressionListNode>(tok.text.data() - tmpl.content.c_str());
2297+ current_block->nodes.emplace_back(expression_list_node);
2298+ current_expression_list = expression_list_node.get();
2299+
2300+ if (!parse_expression(tmpl, Token::Kind::ExpressionClose)) {
2301+ throw_parser_error("expected expression close, got '" + tok.describe() + "'");
2302+ }
2303+ }
2304+ } break;
2305+ case Token::Kind::CommentOpen: {
2306+ get_next_token();
2307+ if (tok.kind != Token::Kind::CommentClose) {
2308+ throw_parser_error("expected comment close, got '" + tok.describe() + "'");
2309+ }
2310+ } break;
2311+ default: {
2312+ throw_parser_error("unexpected token '" + tok.describe() + "'");
2313+ } break;
2314+ }
2315+ }
2316+ }
2317+
2318+public:
2319+ explicit Parser(const ParserConfig& parser_config, const LexerConfig& lexer_config, TemplateStorage& template_storage,
2320+ const FunctionStorage& function_storage)
2321+ : config(parser_config), lexer(lexer_config), template_storage(template_storage), function_storage(function_storage) {}
2322+
2323+ Template parse(std::string_view input, const std::filesystem::path& path) {
2324+ auto result = Template(std::string(input));
2325+ parse_into(result, path);
2326+ return result;
2327+ }
2328+
2329+ void parse_into_template(Template& tmpl, const std::filesystem::path& filename) {
2330+ auto sub_parser = Parser(config, lexer.get_config(), template_storage, function_storage);
2331+ sub_parser.parse_into(tmpl, filename.parent_path());
2332+ }
2333+
2334+ static std::string load_file(const std::filesystem::path& filename) {
2335+ std::ifstream file;
2336+ file.open(filename);
2337+ if (file.fail()) {
2338+ INJA_THROW(FileError("failed accessing file at '" + filename.string() + "'"));
2339+ }
2340+ std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
2341+ return text;
2342+ }
2343+};
2344+
2345+} // namespace finja
2346+
2347+#endif // INCLUDE_FINJA_PARSER_HPP_
2348+
2349+// #include "renderer.hpp"
2350+#ifndef INCLUDE_FINJA_RENDERER_HPP_
2351+#define INCLUDE_FINJA_RENDERER_HPP_
2352+
2353+#include <algorithm>
2354+#include <array>
2355+#include <cctype>
2356+#include <cmath>
2357+#include <cstddef>
2358+#include <memory>
2359+#include <numeric>
2360+#include <ostream>
2361+#include <sstream>
2362+#include <stack>
2363+#include <string>
2364+#include <utility>
2365+#include <vector>
2366+
2367+// #include "config.hpp"
2368+
2369+// #include "exceptions.hpp"
2370+
2371+// #include "function_storage.hpp"
2372+
2373+// #include "node.hpp"
2374+
2375+// #include "template.hpp"
2376+
2377+// #include "throw.hpp"
2378+
2379+// #include "utils.hpp"
2380+
2381+
2382+namespace finja {
2383+
2384+/*!
2385+@brief Escapes HTML
2386+*/
2387+inline std::string htmlescape(const std::string& data) {
2388+ std::string buffer;
2389+ buffer.reserve(static_cast<size_t>(1.1 * data.size()));
2390+ for (size_t pos = 0; pos != data.size(); ++pos) {
2391+ switch (data[pos]) {
2392+ case '&': buffer.append("&amp;"); break;
2393+ case '\"': buffer.append("&quot;"); break;
2394+ case '\'': buffer.append("&apos;"); break;
2395+ case '<': buffer.append("&lt;"); break;
2396+ case '>': buffer.append("&gt;"); break;
2397+ default: buffer.append(&data[pos], 1); break;
2398+ }
2399+ }
2400+ return buffer;
2401+}
2402+
2403+/*!
2404+ * \brief Class for rendering a Template with data.
2405+ */
2406+class Renderer : public NodeVisitor {
2407+ using Op = FunctionStorage::Operation;
2408+
2409+ const RenderConfig config;
2410+ const TemplateStorage& template_storage;
2411+ const FunctionStorage& function_storage;
2412+
2413+ const Template* current_template;
2414+ size_t current_level {0};
2415+ std::vector<const Template*> template_stack;
2416+ std::vector<const BlockStatementNode*> block_statement_stack;
2417+
2418+ const json* data_input;
2419+ std::ostream* output_stream;
2420+
2421+ json additional_data;
2422+ json* current_loop_data = &additional_data["loop"];
2423+
2424+ std::vector<std::shared_ptr<json>> data_tmp_stack;
2425+ std::stack<const json*> data_eval_stack;
2426+ std::stack<const DataNode*> not_found_stack;
2427+
2428+ bool break_rendering {false};
2429+
2430+ static bool truthy(const json* data) {
2431+ if (data == nullptr) {
2432+ return false; // Undefined variables are falsy
2433+ }
2434+ if (data->is_boolean()) {
2435+ return data->get<bool>();
2436+ } else if (data->is_number()) {
2437+ return (*data != 0);
2438+ } else if (data->is_null()) {
2439+ return false;
2440+ }
2441+ return !data->empty();
2442+ }
2443+
2444+ void print_data(const std::shared_ptr<json>& value) {
2445+ if (value->is_string()) {
2446+ if (config.html_autoescape) {
2447+ *output_stream << htmlescape(value->get_ref<const json::string_t&>());
2448+ } else {
2449+ *output_stream << value->get_ref<const json::string_t&>();
2450+ }
2451+ } else if (value->is_number_unsigned()) {
2452+ *output_stream << value->get<const json::number_unsigned_t>();
2453+ } else if (value->is_number_integer()) {
2454+ *output_stream << value->get<const json::number_integer_t>();
2455+ } else if (value->is_null()) {
2456+ } else {
2457+ *output_stream << value->dump();
2458+ }
2459+ }
2460+
2461+ const std::shared_ptr<json> eval_expression_list(const ExpressionListNode& expression_list) {
2462+ if (!expression_list.root) {
2463+ throw_renderer_error("empty expression", expression_list);
2464+ }
2465+
2466+ expression_list.root->accept(*this);
2467+
2468+ if (data_eval_stack.empty()) {
2469+ throw_renderer_error("empty expression", expression_list);
2470+ } else if (data_eval_stack.size() != 1) {
2471+ throw_renderer_error("malformed expression", expression_list);
2472+ }
2473+
2474+ const auto result = data_eval_stack.top();
2475+ data_eval_stack.pop();
2476+
2477+ if (result == nullptr) {
2478+ // Clear not_found_stack to handle undefined variables gracefully (Jinja2 style)
2479+ if (!not_found_stack.empty()) {
2480+ not_found_stack.pop();
2481+ }
2482+ // Return null JSON value for undefined variables
2483+ return std::make_shared<json>(nullptr);
2484+ }
2485+ return std::make_shared<json>(*result);
2486+ }
2487+
2488+ void throw_renderer_error(const std::string& message, const AstNode& node) {
2489+ const SourceLocation loc = get_source_location(current_template->content, node.pos);
2490+ INJA_THROW(RenderError(message, loc));
2491+ }
2492+
2493+ void make_result(const json&& result) {
2494+ auto result_ptr = std::make_shared<json>(result);
2495+ data_tmp_stack.push_back(result_ptr);
2496+ data_eval_stack.push(result_ptr.get());
2497+ }
2498+
2499+ template <size_t N, size_t N_start = 0, bool throw_not_found = true> std::array<const json*, N> get_arguments(const FunctionNode& node) {
2500+ if (node.arguments.size() < N_start + N) {
2501+ throw_renderer_error("function needs " + std::to_string(N_start + N) + " variables, but has only found " + std::to_string(node.arguments.size()), node);
2502+ }
2503+
2504+ for (size_t i = N_start; i < N_start + N; i += 1) {
2505+ node.arguments[i]->accept(*this);
2506+ }
2507+
2508+ if (data_eval_stack.size() < N) {
2509+ throw_renderer_error("function needs " + std::to_string(N) + " variables, but has only found " + std::to_string(data_eval_stack.size()), node);
2510+ }
2511+
2512+ std::array<const json*, N> result;
2513+ for (size_t i = 0; i < N; i += 1) {
2514+ result[N - i - 1] = data_eval_stack.top();
2515+ data_eval_stack.pop();
2516+
2517+ if (!result[N - i - 1]) {
2518+ const auto data_node = not_found_stack.top();
2519+ not_found_stack.pop();
2520+
2521+ if (throw_not_found) {
2522+ throw_renderer_error("variable '" + static_cast<std::string>(data_node->name) + "' not found", *data_node);
2523+ }
2524+ }
2525+ }
2526+ return result;
2527+ }
2528+
2529+ template <bool throw_not_found = true> Arguments get_argument_vector(const FunctionNode& node) {
2530+ const size_t N = node.arguments.size();
2531+ for (const auto& a : node.arguments) {
2532+ a->accept(*this);
2533+ }
2534+
2535+ if (data_eval_stack.size() < N) {
2536+ throw_renderer_error("function needs " + std::to_string(N) + " variables, but has only found " + std::to_string(data_eval_stack.size()), node);
2537+ }
2538+
2539+ Arguments result {N};
2540+ for (size_t i = 0; i < N; i += 1) {
2541+ result[N - i - 1] = data_eval_stack.top();
2542+ data_eval_stack.pop();
2543+
2544+ if (!result[N - i - 1]) {
2545+ const auto data_node = not_found_stack.top();
2546+ not_found_stack.pop();
2547+
2548+ if (throw_not_found) {
2549+ throw_renderer_error("variable '" + static_cast<std::string>(data_node->name) + "' not found", *data_node);
2550+ }
2551+ }
2552+ }
2553+ return result;
2554+ }
2555+
2556+ void visit(const BlockNode& node) override {
2557+ for (const auto& n : node.nodes) {
2558+ n->accept(*this);
2559+
2560+ if (break_rendering) {
2561+ break;
2562+ }
2563+ }
2564+ }
2565+
2566+ void visit(const TextNode& node) override {
2567+ output_stream->write(current_template->content.c_str() + node.pos, node.length);
2568+ }
2569+
2570+ void visit(const ExpressionNode&) override {}
2571+
2572+ void visit(const LiteralNode& node) override {
2573+ data_eval_stack.push(&node.value);
2574+ }
2575+
2576+ void visit(const DataNode& node) override {
2577+ if (additional_data.contains(node.ptr)) {
2578+ data_eval_stack.push(&(additional_data[node.ptr]));
2579+ } else if (data_input->contains(node.ptr)) {
2580+ data_eval_stack.push(&(*data_input)[node.ptr]);
2581+ } else {
2582+ // Try to evaluate as a no-argument callback
2583+ const auto function_data = function_storage.find_function(node.name, 0);
2584+ if (function_data.operation == FunctionStorage::Operation::Callback) {
2585+ Arguments empty_args {};
2586+ const auto value = std::make_shared<json>(function_data.callback(empty_args));
2587+ data_tmp_stack.push_back(value);
2588+ data_eval_stack.push(value.get());
2589+ } else {
2590+ data_eval_stack.push(nullptr);
2591+ not_found_stack.emplace(&node);
2592+ }
2593+ }
2594+ }
2595+
2596+ void visit(const FunctionNode& node) override {
2597+ switch (node.operation) {
2598+ case Op::Not: {
2599+ const auto args = get_arguments<1, 0, false>(node);
2600+ make_result(!truthy(args[0]));
2601+ } break;
2602+ case Op::And: {
2603+ make_result(truthy(get_arguments<1, 0, false>(node)[0]) && truthy(get_arguments<1, 1, false>(node)[0]));
2604+ } break;
2605+ case Op::Or: {
2606+ make_result(truthy(get_arguments<1, 0, false>(node)[0]) || truthy(get_arguments<1, 1, false>(node)[0]));
2607+ } break;
2608+ case Op::In: {
2609+ const auto args = get_arguments<2, 0, false>(node);
2610+ if (args[0] == nullptr || args[1] == nullptr) {
2611+ make_result(false);
2612+ } else {
2613+ make_result(std::find(args[1]->begin(), args[1]->end(), *args[0]) != args[1]->end());
2614+ }
2615+ } break;
2616+ case Op::Equal: {
2617+ const auto args = get_arguments<2, 0, false>(node);
2618+ if (args[0] == nullptr && args[1] == nullptr) {
2619+ make_result(true);
2620+ } else if (args[0] == nullptr || args[1] == nullptr) {
2621+ make_result(false);
2622+ } else {
2623+ make_result(*args[0] == *args[1]);
2624+ }
2625+ } break;
2626+ case Op::NotEqual: {
2627+ const auto args = get_arguments<2, 0, false>(node);
2628+ if (args[0] == nullptr && args[1] == nullptr) {
2629+ make_result(false);
2630+ } else if (args[0] == nullptr || args[1] == nullptr) {
2631+ make_result(true);
2632+ } else {
2633+ make_result(*args[0] != *args[1]);
2634+ }
2635+ } break;
2636+ case Op::Greater: {
2637+ const auto args = get_arguments<2>(node);
2638+ make_result(*args[0] > *args[1]);
2639+ } break;
2640+ case Op::GreaterEqual: {
2641+ const auto args = get_arguments<2>(node);
2642+ make_result(*args[0] >= *args[1]);
2643+ } break;
2644+ case Op::Less: {
2645+ const auto args = get_arguments<2>(node);
2646+ make_result(*args[0] < *args[1]);
2647+ } break;
2648+ case Op::LessEqual: {
2649+ const auto args = get_arguments<2>(node);
2650+ make_result(*args[0] <= *args[1]);
2651+ } break;
2652+ case Op::Add: {
2653+ const auto args = get_arguments<2>(node);
2654+ if (args[0]->is_string() && args[1]->is_string()) {
2655+ make_result(args[0]->get_ref<const json::string_t&>() + args[1]->get_ref<const json::string_t&>());
2656+ } else if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
2657+ make_result(args[0]->get<const json::number_integer_t>() + args[1]->get<const json::number_integer_t>());
2658+ } else {
2659+ make_result(args[0]->get<const json::number_float_t>() + args[1]->get<const json::number_float_t>());
2660+ }
2661+ } break;
2662+ case Op::Subtract: {
2663+ const auto args = get_arguments<2>(node);
2664+ if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
2665+ make_result(args[0]->get<const json::number_integer_t>() - args[1]->get<const json::number_integer_t>());
2666+ } else {
2667+ make_result(args[0]->get<const json::number_float_t>() - args[1]->get<const json::number_float_t>());
2668+ }
2669+ } break;
2670+ case Op::Multiplication: {
2671+ const auto args = get_arguments<2>(node);
2672+ if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
2673+ make_result(args[0]->get<const json::number_integer_t>() * args[1]->get<const json::number_integer_t>());
2674+ } else {
2675+ make_result(args[0]->get<const json::number_float_t>() * args[1]->get<const json::number_float_t>());
2676+ }
2677+ } break;
2678+ case Op::Division: {
2679+ const auto args = get_arguments<2>(node);
2680+ if (args[1]->get<const json::number_float_t>() == 0) {
2681+ throw_renderer_error("division by zero", node);
2682+ }
2683+ make_result(args[0]->get<const json::number_float_t>() / args[1]->get<const json::number_float_t>());
2684+ } break;
2685+ case Op::Power: {
2686+ const auto args = get_arguments<2>(node);
2687+ if (args[0]->is_number_integer() && args[1]->get<const json::number_integer_t>() >= 0) {
2688+ const auto result = static_cast<json::number_integer_t>(std::pow(args[0]->get<const json::number_integer_t>(), args[1]->get<const json::number_integer_t>()));
2689+ make_result(result);
2690+ } else {
2691+ const auto result = std::pow(args[0]->get<const json::number_float_t>(), args[1]->get<const json::number_integer_t>());
2692+ make_result(result);
2693+ }
2694+ } break;
2695+ case Op::Modulo: {
2696+ const auto args = get_arguments<2>(node);
2697+ make_result(args[0]->get<const json::number_integer_t>() % args[1]->get<const json::number_integer_t>());
2698+ } break;
2699+ case Op::AtId: {
2700+ const auto container = get_arguments<1, 0, false>(node)[0];
2701+ node.arguments[1]->accept(*this);
2702+ if (not_found_stack.empty()) {
2703+ throw_renderer_error("could not find element with given name", node);
2704+ }
2705+ const auto id_node = not_found_stack.top();
2706+ not_found_stack.pop();
2707+ data_eval_stack.pop();
2708+ data_eval_stack.push(&container->at(id_node->name));
2709+ } break;
2710+ case Op::At: {
2711+ const auto args = get_arguments<2>(node);
2712+ if (args[0]->is_object()) {
2713+ data_eval_stack.push(&args[0]->at(args[1]->get<std::string>()));
2714+ } else {
2715+ data_eval_stack.push(&args[0]->at(args[1]->get<int>()));
2716+ }
2717+ } break;
2718+ case Op::Capitalize: {
2719+ auto result = get_arguments<1>(node)[0]->get<json::string_t>();
2720+ result[0] = static_cast<char>(::toupper(result[0]));
2721+ std::transform(result.begin() + 1, result.end(), result.begin() + 1, [](char c) { return static_cast<char>(::tolower(c)); });
2722+ make_result(std::move(result));
2723+ } break;
2724+ case Op::Default: {
2725+ const auto test_arg = get_arguments<1, 0, false>(node)[0];
2726+ data_eval_stack.push((test_arg != nullptr) ? test_arg : get_arguments<1, 1>(node)[0]);
2727+ } break;
2728+ case Op::DivisibleBy: {
2729+ const auto args = get_arguments<2>(node);
2730+ const auto divisor = args[1]->get<const json::number_integer_t>();
2731+ make_result((divisor != 0) && (args[0]->get<const json::number_integer_t>() % divisor == 0));
2732+ } break;
2733+ case Op::Even: {
2734+ make_result(get_arguments<1>(node)[0]->get<const json::number_integer_t>() % 2 == 0);
2735+ } break;
2736+ case Op::Exists: {
2737+ auto&& name = get_arguments<1>(node)[0]->get_ref<const json::string_t&>();
2738+ make_result(data_input->contains(json::json_pointer(DataNode::convert_dot_to_ptr(name))));
2739+ } break;
2740+ case Op::ExistsInObject: {
2741+ const auto args = get_arguments<2>(node);
2742+ auto&& name = args[1]->get_ref<const json::string_t&>();
2743+ make_result(args[0]->find(name) != args[0]->end());
2744+ } break;
2745+ case Op::First: {
2746+ const auto result = &get_arguments<1>(node)[0]->front();
2747+ data_eval_stack.push(result);
2748+ } break;
2749+ case Op::Float: {
2750+ make_result(std::stod(get_arguments<1>(node)[0]->get_ref<const json::string_t&>()));
2751+ } break;
2752+ case Op::Int: {
2753+ make_result(std::stoi(get_arguments<1>(node)[0]->get_ref<const json::string_t&>()));
2754+ } break;
2755+ case Op::Last: {
2756+ const auto result = &get_arguments<1>(node)[0]->back();
2757+ data_eval_stack.push(result);
2758+ } break;
2759+ case Op::Length: {
2760+ const auto val = get_arguments<1>(node)[0];
2761+ if (val->is_string()) {
2762+ make_result(val->get_ref<const json::string_t&>().length());
2763+ } else {
2764+ make_result(val->size());
2765+ }
2766+ } break;
2767+ case Op::Lower: {
2768+ auto result = get_arguments<1>(node)[0]->get<json::string_t>();
2769+ std::transform(result.begin(), result.end(), result.begin(), [](char c) { return static_cast<char>(::tolower(c)); });
2770+ make_result(std::move(result));
2771+ } break;
2772+ case Op::Max: {
2773+ const auto args = get_arguments<1>(node);
2774+ const auto result = std::max_element(args[0]->begin(), args[0]->end());
2775+ data_eval_stack.push(&(*result));
2776+ } break;
2777+ case Op::Min: {
2778+ const auto args = get_arguments<1>(node);
2779+ const auto result = std::min_element(args[0]->begin(), args[0]->end());
2780+ data_eval_stack.push(&(*result));
2781+ } break;
2782+ case Op::Odd: {
2783+ make_result(get_arguments<1>(node)[0]->get<const json::number_integer_t>() % 2 != 0);
2784+ } break;
2785+ case Op::Range: {
2786+ std::vector<int> result(get_arguments<1>(node)[0]->get<const json::number_integer_t>());
2787+ std::iota(result.begin(), result.end(), 0);
2788+ make_result(std::move(result));
2789+ } break;
2790+ case Op::Replace: {
2791+ const auto args = get_arguments<3>(node);
2792+ auto result = args[0]->get<std::string>();
2793+ replace_substring(result, args[1]->get<std::string>(), args[2]->get<std::string>());
2794+ make_result(std::move(result));
2795+ } break;
2796+ case Op::Round: {
2797+ const auto args = get_arguments<2>(node);
2798+ const auto precision = args[1]->get<const json::number_integer_t>();
2799+ const double result = std::round(args[0]->get<const json::number_float_t>() * std::pow(10.0, precision)) / std::pow(10.0, precision);
2800+ if (precision == 0) {
2801+ make_result(static_cast<int>(result));
2802+ } else {
2803+ make_result(result);
2804+ }
2805+ } break;
2806+ case Op::Sort: {
2807+ auto result_ptr = std::make_shared<json>(get_arguments<1>(node)[0]->get<std::vector<json>>());
2808+ std::sort(result_ptr->begin(), result_ptr->end());
2809+ data_tmp_stack.push_back(result_ptr);
2810+ data_eval_stack.push(result_ptr.get());
2811+ } break;
2812+ case Op::Upper: {
2813+ auto result = get_arguments<1>(node)[0]->get<json::string_t>();
2814+ std::transform(result.begin(), result.end(), result.begin(), [](char c) { return static_cast<char>(::toupper(c)); });
2815+ make_result(std::move(result));
2816+ } break;
2817+ case Op::IsBoolean: {
2818+ make_result(get_arguments<1>(node)[0]->is_boolean());
2819+ } break;
2820+ case Op::IsNumber: {
2821+ make_result(get_arguments<1>(node)[0]->is_number());
2822+ } break;
2823+ case Op::IsInteger: {
2824+ make_result(get_arguments<1>(node)[0]->is_number_integer());
2825+ } break;
2826+ case Op::IsFloat: {
2827+ make_result(get_arguments<1>(node)[0]->is_number_float());
2828+ } break;
2829+ case Op::IsObject: {
2830+ make_result(get_arguments<1>(node)[0]->is_object());
2831+ } break;
2832+ case Op::IsArray: {
2833+ make_result(get_arguments<1>(node)[0]->is_array());
2834+ } break;
2835+ case Op::IsString: {
2836+ make_result(get_arguments<1>(node)[0]->is_string());
2837+ } break;
2838+ case Op::Callback: {
2839+ auto args = get_argument_vector(node);
2840+ make_result(node.callback(args));
2841+ } break;
2842+ case Op::Super: {
2843+ const auto args = get_argument_vector(node);
2844+ const size_t old_level = current_level;
2845+ const size_t level_diff = (args.size() == 1) ? args[0]->get<int>() : 1;
2846+ const size_t level = current_level + level_diff;
2847+
2848+ if (block_statement_stack.empty()) {
2849+ throw_renderer_error("super() call is not within a block", node);
2850+ }
2851+
2852+ if (level < 1 || level > template_stack.size() - 1) {
2853+ throw_renderer_error("level of super() call does not match parent templates (between 1 and " + std::to_string(template_stack.size() - 1) + ")", node);
2854+ }
2855+
2856+ const auto current_block_statement = block_statement_stack.back();
2857+ const Template* new_template = template_stack.at(level);
2858+ const Template* old_template = current_template;
2859+ const auto block_it = new_template->block_storage.find(current_block_statement->name);
2860+ if (block_it != new_template->block_storage.end()) {
2861+ current_template = new_template;
2862+ current_level = level;
2863+ block_it->second->block.accept(*this);
2864+ current_level = old_level;
2865+ current_template = old_template;
2866+ } else {
2867+ throw_renderer_error("could not find block with name '" + current_block_statement->name + "'", node);
2868+ }
2869+ make_result(nullptr);
2870+ } break;
2871+ case Op::Join: {
2872+ const auto args = get_arguments<2>(node);
2873+ const auto separator = args[1]->get<json::string_t>();
2874+ std::ostringstream os;
2875+ std::string sep;
2876+ for (const auto& value : *args[0]) {
2877+ os << sep;
2878+ if (value.is_string()) {
2879+ os << value.get<std::string>(); // otherwise the value is surrounded with ""
2880+ } else {
2881+ os << value.dump();
2882+ }
2883+ sep = separator;
2884+ }
2885+ make_result(os.str());
2886+ } break;
2887+ case Op::None:
2888+ break;
2889+ }
2890+ }
2891+
2892+ void visit(const ExpressionListNode& node) override {
2893+ print_data(eval_expression_list(node));
2894+ }
2895+
2896+ void visit(const StatementNode&) override {}
2897+
2898+ void visit(const ForStatementNode&) override {}
2899+
2900+ void visit(const ForArrayStatementNode& node) override {
2901+ const auto result = eval_expression_list(node.condition);
2902+ if (!result->is_array()) {
2903+ throw_renderer_error("object must be an array", node);
2904+ }
2905+
2906+ if (!current_loop_data->empty()) {
2907+ auto tmp = *current_loop_data; // Because of clang-3
2908+ (*current_loop_data)["parent"] = std::move(tmp);
2909+ }
2910+
2911+ size_t index = 0;
2912+ (*current_loop_data)["is_first"] = true;
2913+ (*current_loop_data)["is_last"] = (result->size() <= 1);
2914+ for (auto it = result->begin(); it != result->end(); ++it) {
2915+ additional_data[static_cast<std::string>(node.value)] = *it;
2916+
2917+ (*current_loop_data)["index"] = index;
2918+ (*current_loop_data)["index1"] = index + 1;
2919+ if (index == 1) {
2920+ (*current_loop_data)["is_first"] = false;
2921+ }
2922+ if (index == result->size() - 1) {
2923+ (*current_loop_data)["is_last"] = true;
2924+ }
2925+
2926+ node.body.accept(*this);
2927+ ++index;
2928+ }
2929+
2930+ additional_data[static_cast<std::string>(node.value)].clear();
2931+ if (!(*current_loop_data)["parent"].empty()) {
2932+ const auto tmp = (*current_loop_data)["parent"];
2933+ *current_loop_data = tmp;
2934+ } else {
2935+ current_loop_data = &additional_data["loop"];
2936+ }
2937+ }
2938+
2939+ void visit(const ForObjectStatementNode& node) override {
2940+ const auto result = eval_expression_list(node.condition);
2941+ if (!result->is_object()) {
2942+ throw_renderer_error("object must be an object", node);
2943+ }
2944+
2945+ if (!current_loop_data->empty()) {
2946+ (*current_loop_data)["parent"] = std::move(*current_loop_data);
2947+ }
2948+
2949+ size_t index = 0;
2950+ (*current_loop_data)["is_first"] = true;
2951+ (*current_loop_data)["is_last"] = (result->size() <= 1);
2952+ for (auto it = result->begin(); it != result->end(); ++it) {
2953+ additional_data[static_cast<std::string>(node.key)] = it.key();
2954+ additional_data[static_cast<std::string>(node.value)] = it.value();
2955+
2956+ (*current_loop_data)["index"] = index;
2957+ (*current_loop_data)["index1"] = index + 1;
2958+ if (index == 1) {
2959+ (*current_loop_data)["is_first"] = false;
2960+ }
2961+ if (index == result->size() - 1) {
2962+ (*current_loop_data)["is_last"] = true;
2963+ }
2964+
2965+ node.body.accept(*this);
2966+ ++index;
2967+ }
2968+
2969+ additional_data[static_cast<std::string>(node.key)].clear();
2970+ additional_data[static_cast<std::string>(node.value)].clear();
2971+ if (!(*current_loop_data)["parent"].empty()) {
2972+ *current_loop_data = std::move((*current_loop_data)["parent"]);
2973+ } else {
2974+ current_loop_data = &additional_data["loop"];
2975+ }
2976+ }
2977+
2978+ void visit(const IfStatementNode& node) override {
2979+ const auto result = eval_expression_list(node.condition);
2980+ if (truthy(result.get())) {
2981+ node.true_statement.accept(*this);
2982+ } else if (node.has_false_statement) {
2983+ node.false_statement.accept(*this);
2984+ }
2985+ }
2986+
2987+ void visit(const IncludeStatementNode& node) override {
2988+ auto sub_renderer = Renderer(config, template_storage, function_storage);
2989+ const auto included_template_it = template_storage.find(node.file);
2990+ if (included_template_it != template_storage.end()) {
2991+ sub_renderer.render_to(*output_stream, included_template_it->second, *data_input, &additional_data);
2992+ } else if (config.throw_at_missing_includes) {
2993+ throw_renderer_error("include '" + node.file + "' not found", node);
2994+ }
2995+ }
2996+
2997+ void visit(const ExtendsStatementNode& node) override {
2998+ const auto included_template_it = template_storage.find(node.file);
2999+ if (included_template_it != template_storage.end()) {
3000+ const Template* parent_template = &included_template_it->second;
3001+ render_to(*output_stream, *parent_template, *data_input, &additional_data);
3002+ break_rendering = true;
3003+ } else if (config.throw_at_missing_includes) {
3004+ throw_renderer_error("extends '" + node.file + "' not found", node);
3005+ }
3006+ }
3007+
3008+ void visit(const BlockStatementNode& node) override {
3009+ const size_t old_level = current_level;
3010+ current_level = 0;
3011+ current_template = template_stack.front();
3012+ const auto block_it = current_template->block_storage.find(node.name);
3013+ if (block_it != current_template->block_storage.end()) {
3014+ block_statement_stack.emplace_back(&node);
3015+ block_it->second->block.accept(*this);
3016+ block_statement_stack.pop_back();
3017+ }
3018+ current_level = old_level;
3019+ current_template = template_stack.back();
3020+ }
3021+
3022+ void visit(const SetStatementNode& node) override {
3023+ std::string ptr = node.key;
3024+ replace_substring(ptr, ".", "/");
3025+ ptr = "/" + ptr;
3026+ additional_data[json::json_pointer(ptr)] = *eval_expression_list(node.expression);
3027+ }
3028+
3029+public:
3030+ explicit Renderer(const RenderConfig& config, const TemplateStorage& template_storage, const FunctionStorage& function_storage)
3031+ : config(config), template_storage(template_storage), function_storage(function_storage) {}
3032+
3033+ void render_to(std::ostream& os, const Template& tmpl, const json& data, json* loop_data = nullptr) {
3034+ output_stream = &os;
3035+ current_template = &tmpl;
3036+ data_input = &data;
3037+ if (loop_data != nullptr) {
3038+ additional_data = *loop_data;
3039+ current_loop_data = &additional_data["loop"];
3040+ }
3041+
3042+ template_stack.emplace_back(current_template);
3043+ current_template->root.accept(*this);
3044+
3045+ data_tmp_stack.clear();
3046+ }
3047+};
3048+
3049+} // namespace finja
3050+
3051+#endif // INCLUDE_FINJA_RENDERER_HPP_
3052+
3053+// #include "template.hpp"
3054+
3055+// #include "throw.hpp"
3056+
3057+
3058+namespace finja {
3059+
3060+/*!
3061+ * \brief Class for changing the configuration.
3062+ */
3063+class Environment {
3064+ FunctionStorage function_storage;
3065+ TemplateStorage template_storage;
3066+
3067+protected:
3068+ LexerConfig lexer_config;
3069+ ParserConfig parser_config;
3070+ RenderConfig render_config;
3071+
3072+ std::filesystem::path input_path;
3073+ std::filesystem::path input_path_filename;
3074+ std::filesystem::path output_path;
3075+
3076+public:
3077+ Environment(): Environment("") {}
3078+ explicit Environment(const std::filesystem::path& global_path): input_path(global_path), output_path(global_path) {}
3079+ Environment(const std::filesystem::path& input_path, const std::filesystem::path& output_path): input_path(input_path), output_path(output_path) {}
3080+
3081+ /// Sets the opener and closer for template statements
3082+ void set_statement(const std::string& open, const std::string& close) {
3083+ lexer_config.statement_open = open;
3084+ lexer_config.statement_open_no_lstrip = open + "+";
3085+ lexer_config.statement_open_force_lstrip = open + "-";
3086+ lexer_config.statement_close = close;
3087+ lexer_config.statement_close_force_rstrip = "-" + close;
3088+ lexer_config.update_open_chars();
3089+ }
3090+
3091+ /// Sets the opener for template line statements
3092+ void set_line_statement(const std::string& open) {
3093+ lexer_config.line_statement = open;
3094+ lexer_config.update_open_chars();
3095+ }
3096+
3097+ /// Sets the opener and closer for template expressions
3098+ void set_expression(const std::string& open, const std::string& close) {
3099+ lexer_config.expression_open = open;
3100+ lexer_config.expression_open_force_lstrip = open + "-";
3101+ lexer_config.expression_close = close;
3102+ lexer_config.expression_close_force_rstrip = "-" + close;
3103+ lexer_config.update_open_chars();
3104+ }
3105+
3106+ /// Sets the opener and closer for template comments
3107+ void set_comment(const std::string& open, const std::string& close) {
3108+ lexer_config.comment_open = open;
3109+ lexer_config.comment_open_force_lstrip = open + "-";
3110+ lexer_config.comment_close = close;
3111+ lexer_config.comment_close_force_rstrip = "-" + close;
3112+ lexer_config.update_open_chars();
3113+ }
3114+
3115+ /// Sets whether to remove the first newline after a block
3116+ void set_trim_blocks(bool trim_blocks) {
3117+ lexer_config.trim_blocks = trim_blocks;
3118+ }
3119+
3120+ /// Sets whether to strip the spaces and tabs from the start of a line to a block
3121+ void set_lstrip_blocks(bool lstrip_blocks) {
3122+ lexer_config.lstrip_blocks = lstrip_blocks;
3123+ }
3124+
3125+ /// Sets the element notation syntax
3126+ void set_search_included_templates_in_files(bool search_in_files) {
3127+ parser_config.search_included_templates_in_files = search_in_files;
3128+ }
3129+
3130+ /// Sets whether a missing include will throw an error
3131+ void set_throw_at_missing_includes(bool will_throw) {
3132+ render_config.throw_at_missing_includes = will_throw;
3133+ }
3134+
3135+ /// Sets whether we'll automatically perform HTML escape
3136+ void set_html_autoescape(bool will_escape) {
3137+ render_config.html_autoescape = will_escape;
3138+ }
3139+
3140+ Template parse(std::string_view input) {
3141+ Parser parser(parser_config, lexer_config, template_storage, function_storage);
3142+ return parser.parse(input, input_path);
3143+ }
3144+
3145+ Template parse_template(const std::filesystem::path& filename) {
3146+ Parser parser(parser_config, lexer_config, template_storage, function_storage);
3147+ CURRENT_FILENAME = input_path / std::filesystem::path(filename).filename();
3148+ auto result = Template(Parser::load_file(input_path / filename));
3149+ parser.parse_into_template(result, (input_path / filename).string());
3150+ return result;
3151+ }
3152+
3153+ Template parse_file(const std::filesystem::path& filename) {
3154+ return parse_template(filename);
3155+ }
3156+
3157+ std::string render(std::string_view input, const json& data) {
3158+ return render(parse(input), data);
3159+ }
3160+
3161+ std::string render(const Template& tmpl, const json& data) {
3162+ std::stringstream os;
3163+ render_to(os, tmpl, data);
3164+ return os.str();
3165+ }
3166+
3167+ std::string render_file(const std::filesystem::path& filename, const json& data) {
3168+ return render(parse_template(filename), data);
3169+ }
3170+
3171+ std::string render_file_with_json_file(const std::filesystem::path& filename, const std::string& filename_data) {
3172+ const json data = load_json(filename_data);
3173+ return render_file(filename, data);
3174+ }
3175+
3176+ void write(const std::filesystem::path& filename, const json& data, const std::string& filename_out) {
3177+ std::ofstream file(output_path / filename_out);
3178+ file << render_file(filename, data);
3179+ file.close();
3180+ }
3181+
3182+ void write(const Template& temp, const json& data, const std::string& filename_out) {
3183+ std::ofstream file(output_path / filename_out);
3184+ file << render(temp, data);
3185+ file.close();
3186+ }
3187+
3188+ void write_with_json_file(const std::filesystem::path& filename, const std::string& filename_data, const std::string& filename_out) {
3189+ const json data = load_json(filename_data);
3190+ write(filename, data, filename_out);
3191+ }
3192+
3193+ void write_with_json_file(const Template& temp, const std::string& filename_data, const std::string& filename_out) {
3194+ const json data = load_json(filename_data);
3195+ write(temp, data, filename_out);
3196+ }
3197+
3198+ std::ostream& render_to(std::ostream& os, const Template& tmpl, const json& data) {
3199+ Renderer(render_config, template_storage, function_storage).render_to(os, tmpl, data);
3200+ return os;
3201+ }
3202+
3203+ std::ostream& render_to(std::ostream& os, const std::string_view input, const json& data) {
3204+ return render_to(os, parse(input), data);
3205+ }
3206+
3207+ std::string load_file(const std::string& filename) {
3208+ const Parser parser(parser_config, lexer_config, template_storage, function_storage);
3209+ return Parser::load_file(input_path / filename);
3210+ }
3211+
3212+ json load_json(const std::string& filename) {
3213+ std::ifstream file;
3214+ file.open(input_path / filename);
3215+ if (file.fail()) {
3216+ INJA_THROW(FileError("failed accessing file at '" + (input_path / filename).string() + "'"));
3217+ }
3218+
3219+ return json::parse(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
3220+ }
3221+
3222+ /*!
3223+ @brief Adds a variadic callback
3224+ */
3225+ void add_callback(const std::string& name, const CallbackFunction& callback) {
3226+ add_callback(name, -1, callback);
3227+ }
3228+
3229+ /*!
3230+ @brief Adds a variadic void callback
3231+ */
3232+ void add_void_callback(const std::string& name, const VoidCallbackFunction& callback) {
3233+ add_void_callback(name, -1, callback);
3234+ }
3235+
3236+ /*!
3237+ @brief Adds a callback with given number or arguments
3238+ */
3239+ void add_callback(const std::string& name, int num_args, const CallbackFunction& callback) {
3240+ function_storage.add_callback(name, num_args, callback);
3241+ }
3242+
3243+ /*!
3244+ @brief Adds a void callback with given number or arguments
3245+ */
3246+ void add_void_callback(const std::string& name, int num_args, const VoidCallbackFunction& callback) {
3247+ function_storage.add_callback(name, num_args, [callback](Arguments& args) {
3248+ callback(args);
3249+ return json();
3250+ });
3251+ }
3252+
3253+ /** Includes a template with a given name into the environment.
3254+ * Then, a template can be rendered in another template using the
3255+ * include "<name>" syntax.
3256+ */
3257+ void include_template(const std::string& name, const Template& tmpl) {
3258+ template_storage[name] = tmpl;
3259+ }
3260+
3261+ /*!
3262+ @brief Sets a function that is called when an included file is not found
3263+ */
3264+ void set_include_callback(const std::function<Template(const std::filesystem::path&, const std::string&)>& callback) {
3265+ parser_config.include_callback = callback;
3266+ }
3267+};
3268+
3269+/*!
3270+@brief render with default settings to a string
3271+*/
3272+inline std::string render(std::string_view input, const json& data) {
3273+ return Environment().render(input, data);
3274+}
3275+
3276+/*!
3277+@brief render with default settings to the given output stream
3278+*/
3279+inline void render_to(std::ostream& os, std::string_view input, const json& data) {
3280+ Environment env;
3281+ env.render_to(os, env.parse(input), data);
3282+}
3283+
3284+} // namespace finja
3285+
3286+#endif // INCLUDE_FINJA_ENVIRONMENT_HPP_
3287+
3288+// #include "exceptions.hpp"
3289+
3290+// #include "parser.hpp"
3291+
3292+// #include "renderer.hpp"
3293+
3294+// #include "template.hpp"
3295+
3296+
3297+#endif // INCLUDE_FINJA_FINJA_HPP_
3298diff --git a/tests/test_undefined.cpp b/tests/test_undefined.cpp
3299new file mode 100644
3300index 0000000..88683b7
3301--- /dev/null
3302+++ b/tests/test_undefined.cpp
3303@@ -0,0 +1,103 @@
3304+#include <iostream>
3305+#include "finja/finja.hpp"
3306+
3307+using namespace finja;
3308+
3309+int main()
3310+{
3311+ finja::Environment env;
3312+ env.set_html_autoescape(false);
3313+
3314+ json data;
3315+ data["architecture"] = "world";
3316+ data["test"] = true;
3317+ data["bla"] = "hello";
3318+ data["name"] = "Inja";
3319+
3320+ // test 1: single quotes
3321+ std::string test1 = env.render("{{ set architecture = def(architecture, \"lol\") }} architecture: {{ architecture }}", data);
3322+ std::cout << "Test 1 OK: " << test1 << "\n";
3323+
3324+ // test 2
3325+ std::string test2 = env.render(
3326+ "{{ if test }}condition is true and architecture was {{architecture}}{{ else }}condition is false{{ endif }}",
3327+ data
3328+ );
3329+ assert(test2 == "condition is true and architecture was world");
3330+ std::cout << "Test 2 OK: " << test2 << "\n";
3331+
3332+ // test 3
3333+ std::string test3 = env.render("{{ if bla == \"hello\" }}hello{{ else }}no{{ endif }}", data);
3334+ assert(test3 == "hello");
3335+ std::cout << "Test 3 OK: " << test3 << "\n";
3336+
3337+ // test 4: old syntax should remain raw text
3338+ std::string test4 = env.render("{% set architecture = 'arm64' %} architecture: {{ architecture }}", data);
3339+ assert(test4 == "{% set architecture = 'arm64' %} architecture: world");
3340+ std::cout << "Test 4 OK: " << test4 << "\n";
3341+
3342+ // test 5: hmm
3343+ std::string test5 = env.render("Hello {{- name -}} !", data);
3344+ std::cout << "Test 5: " << test5 << "\n";
3345+ assert(test5 == "HelloInja!");
3346+
3347+ // test 6: should render without whitespace, stripping behind a statement or expression also removes any newlines.
3348+ std::string test6 = env.render("{{ if test -}} I was there{{ endif -}} !", data);
3349+ std::cout << "Test 6: " << test6 << "\n";
3350+ assert(test6 == "I was there!");
3351+
3352+ // test 7: handle whitespace correctly
3353+ std::string s = R"({{- set architecture = def(architecture, "arm64") -}}
3354+{{- set debian_suite = def(debian_suite, "excalibur") -}}
3355+{{- set environment = def(environment, "hildon") -}}
3356+{{- set device = def(device, "unknown_device") -}}
3357+{{- set username = def(username, "user") -}}
3358+{{- set password = def(password, "user") -}}
3359+{{- set hostname = def(hostname, "leste") -}}
3360+{{- set device = def(device, "amd64") -}}
3361+{{- set contrib = def(contrib, "true") -}}
3362+{{- set nonfree = def(nonfree, "false") -}}
3363+{{- set zram = def(zram, "false") -}}
3364+{{- set rootfs = def(rootfs, "rootfs.tar.gz") -}}
3365+{{- set mirror = def(mirror, "https://pkgmaster.devuan.org/merged") -}}
3366+architecture: {{ architecture }}
3367+
3368+actions:
3369+ - action: debootstrap
3370+ include: devuan-keyring
3371+ suite: {{ debian_suite }}
3372+ components:
3373+ - main
3374+{{ if contrib }}
3375+ - contrib
3376+{{ endif }}
3377+{{ if nonfree }}
3378+ - non-free
3379+ - non-free-firmware
3380+{{ endif }}
3381+ mirror: {{ if not contrib }}{{ mirror }}{{ else }}lol{{ endif }}
3382+ variant: minbase
3383+ keyring-package: devuan-keyring
3384+ keyring-file: /recipes/keyrings/devuan-keyring/keyrings/devuan-archive-keyring.gpg)";
3385+
3386+ const std::string test7 = env.render(s, data);
3387+ const std::string expected = R"(architecture: world
3388+
3389+actions:
3390+ - action: debootstrap
3391+ include: devuan-keyring
3392+ suite: excalibur
3393+ components:
3394+ - main
3395+ - contrib
3396+ - non-free
3397+ - non-free-firmware
3398+ mirror: lol
3399+ variant: minbase
3400+ keyring-package: devuan-keyring
3401+ keyring-file: /recipes/keyrings/devuan-keyring/keyrings/devuan-archive-keyring.gpg)";
3402+
3403+ std::cout << "Test 7: \n" << test7 << "\n";
3404+ assert(test7 == expected);
3405+ return 0;
3406+}
3407\ No newline at end of file