diff --git a/.gitignore b/.gitignore index 7068ef9..5e6486c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ dist # Coveralls repo token .coveralls.yml + +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt index e3e295e..7332d77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,8 @@ endif() ## set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") -set(INJA_SOURCE_DIR src) +set(INJA_INCLUDE_DIR include) +set(INJA_SINGLE_INCLUDE_DIR single_include) set(INJA_HEADER_INSTALL_DIR include) if(WIN32 AND MSVC AND MSVC_VERSION LESS 1900) @@ -50,26 +51,39 @@ if(BUILD_UNIT_TESTS) endif() +## +## AMALGAMATE +## amalgamate header files into single_include +## +execute_process(COMMAND python3 amalgamate/amalgamate.py -c amalgamate/config.json -s include + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) + + ## ## TARGETS ## Build targets for the interface library ## add_library(inja INTERFACE) target_include_directories(inja INTERFACE - $ + $ $ ) + + +add_library(inja_single INTERFACE) +target_include_directories(inja_single INTERFACE + $ + $ + $ +) + + if(HUNTER_ENABLED) # Use Hunter to manage dependencies # Add JSON package hunter_add_package(nlohmann_json) find_package(nlohmann_json CONFIG REQUIRED) # Add dependencies to target target_link_libraries(inja INTERFACE nlohmann_json) -else() - target_include_directories(inja INTERFACE - $ - $ - ) endif() @@ -106,7 +120,7 @@ install( ) install( - FILES ${INJA_SOURCE_DIR}/inja.hpp + FILES ${INJA_INCLUDE_DIR}/inja.hpp DESTINATION "${include_install_dir}" ) diff --git a/amalgamate/LICENSE.md b/amalgamate/LICENSE.md new file mode 100644 index 0000000..7fe9cf0 --- /dev/null +++ b/amalgamate/LICENSE.md @@ -0,0 +1,27 @@ +amalgamate.py - Amalgamate C source and header files +Copyright (c) 2012, Erik Edlund + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Erik Edlund, nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/amalgamate/amalgamate.py b/amalgamate/amalgamate.py new file mode 100755 index 0000000..138017b --- /dev/null +++ b/amalgamate/amalgamate.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python + +# amalgamate.py - Amalgamate C source and header files. +# Copyright (c) 2012, Erik Edlund +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of Erik Edlund, nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import datetime +import json +import os +import re +import sys + +class Amalgamation(object): + + # Prepends self.source_path to file_path if needed. + def actual_path(self, file_path): + if not os.path.isabs(file_path): + file_path = os.path.join(self.source_path, file_path) + return file_path + + # Search included file_path in self.include_paths and + # in source_dir if specified. + def find_included_file(self, file_path, source_dir): + search_dirs = self.include_paths[:] + if source_dir: + search_dirs.insert(0, source_dir) + + for search_dir in search_dirs: + search_path = os.path.join(search_dir, file_path) + if os.path.isfile(self.actual_path(search_path)): + return search_path + return None + + def __init__(self, args): + with open(args.config, 'r') as f: + config = json.loads(f.read()) + for key in config: + setattr(self, key, config[key]) + + self.verbose = args.verbose == "yes" + self.prologue = args.prologue + self.source_path = args.source_path + self.included_files = [] + + # Generate the amalgamation and write it to the target file. + def generate(self): + amalgamation = "" + + if self.prologue: + with open(self.prologue, 'r') as f: + amalgamation += datetime.datetime.now().strftime(f.read()) + + if self.verbose: + print("Config:") + print(" target = {0}".format(self.target)) + print(" working_dir = {0}".format(os.getcwd())) + print(" include_paths = {0}".format(self.include_paths)) + print("Creating amalgamation:") + for file_path in self.sources: + # Do not check the include paths while processing the source + # list, all given source paths must be correct. + actual_path = self.actual_path(file_path) + print(" - processing \"{0}\"".format(file_path)) + t = TranslationUnit(file_path, self, True) + amalgamation += t.content + + with open(self.target, 'w') as f: + f.write(amalgamation) + + print("...done!\n") + if self.verbose: + print("Files processed: {0}".format(self.sources)) + print("Files included: {0}".format(self.included_files)) + print("") + +class TranslationUnit(object): + + # // C++ comment. + cpp_comment_pattern = re.compile(r"//.*?\n") + + # /* C comment. */ + c_comment_pattern = re.compile(r"/\*.*?\*/", re.S) + + # "complex \"stri\\\ng\" value". + string_pattern = re.compile("[^']" r'".*?(?<=[^\\])"', re.S) + + # Handle simple include directives. Support for advanced + # directives where macros and defines needs to expanded is + # not a concern right now. + include_pattern = re.compile( + r'#\s*include\s+(<|")(?P.*?)("|>)', re.S) + + # #pragma once + pragma_once_pattern = re.compile(r'#\s*pragma\s+once', re.S) + + # Search for pattern in self.content, add the match to + # contexts if found and update the index accordingly. + def _search_content(self, index, pattern, contexts): + match = pattern.search(self.content, index) + if match: + contexts.append(match) + return match.end() + return index + 2 + + # Return all the skippable contexts, i.e., comments and strings + def _find_skippable_contexts(self): + # Find contexts in the content in which a found include + # directive should not be processed. + skippable_contexts = [] + + # Walk through the content char by char, and try to grab + # skippable contexts using regular expressions when found. + i = 1 + content_len = len(self.content) + while i < content_len: + j = i - 1 + current = self.content[i] + previous = self.content[j] + + if current == '"': + # String value. + i = self._search_content(j, self.string_pattern, + skippable_contexts) + elif current == '*' and previous == '/': + # C style comment. + i = self._search_content(j, self.c_comment_pattern, + skippable_contexts) + elif current == '/' and previous == '/': + # C++ style comment. + i = self._search_content(j, self.cpp_comment_pattern, + skippable_contexts) + else: + # Skip to the next char. + i += 1 + + return skippable_contexts + + # Returns True if the match is within list of other matches + def _is_within(self, match, matches): + for m in matches: + if match.start() > m.start() and \ + match.end() < m.end(): + return True + return False + + # Removes pragma once from content + def _process_pragma_once(self): + content_len = len(self.content) + if content_len < len("#include "): + return 0 + + # Find contexts in the content in which a found include + # directive should not be processed. + skippable_contexts = self._find_skippable_contexts() + + pragmas = [] + pragma_once_match = self.pragma_once_pattern.search(self.content) + while pragma_once_match: + if not self._is_within(pragma_once_match, skippable_contexts): + pragmas.append(pragma_once_match) + + pragma_once_match = self.pragma_once_pattern.search(self.content, + pragma_once_match.end()) + + # Handle all collected pragma once directives. + prev_end = 0 + tmp_content = '' + for pragma_match in pragmas: + tmp_content += self.content[prev_end:pragma_match.start()] + prev_end = pragma_match.end() + tmp_content += self.content[prev_end:] + self.content = tmp_content + + # Include all trivial #include directives into self.content. + def _process_includes(self): + content_len = len(self.content) + if content_len < len("#include "): + return 0 + + # Find contexts in the content in which a found include + # directive should not be processed. + skippable_contexts = self._find_skippable_contexts() + + # Search for include directives in the content, collect those + # which should be included into the content. + includes = [] + include_match = self.include_pattern.search(self.content) + while include_match: + if not self._is_within(include_match, skippable_contexts): + include_path = include_match.group("path") + search_same_dir = include_match.group(1) == '"' + found_included_path = self.amalgamation.find_included_file( + include_path, self.file_dir if search_same_dir else None) + if found_included_path: + includes.append((include_match, found_included_path)) + + include_match = self.include_pattern.search(self.content, + include_match.end()) + + # Handle all collected include directives. + prev_end = 0 + tmp_content = '' + for include in includes: + include_match, found_included_path = include + tmp_content += self.content[prev_end:include_match.start()] + tmp_content += "// {0}\n".format(include_match.group(0)) + if not found_included_path in self.amalgamation.included_files: + t = TranslationUnit(found_included_path, self.amalgamation, False) + tmp_content += t.content + prev_end = include_match.end() + tmp_content += self.content[prev_end:] + self.content = tmp_content + + return len(includes) + + # Make all content processing + def _process(self): + if not self.is_root: + self._process_pragma_once() + self._process_includes() + + def __init__(self, file_path, amalgamation, is_root): + self.file_path = file_path + self.file_dir = os.path.dirname(file_path) + self.amalgamation = amalgamation + self.is_root = is_root + + self.amalgamation.included_files.append(self.file_path) + + actual_path = self.amalgamation.actual_path(file_path) + if not os.path.isfile(actual_path): + raise IOError("File not found: \"{0}\"".format(file_path)) + with open(actual_path, 'r') as f: + self.content = f.read() + self._process() + +def main(): + description = "Amalgamate C source and header files." + usage = " ".join([ + "amalgamate.py", + "[-v]", + "-c path/to/config.json", + "-s path/to/source/dir", + "[-p path/to/prologue.(c|h)]" + ]) + argsparser = argparse.ArgumentParser( + description=description, usage=usage) + + argsparser.add_argument("-v", "--verbose", dest="verbose", + choices=["yes", "no"], metavar="", help="be verbose") + + argsparser.add_argument("-c", "--config", dest="config", + required=True, metavar="", help="path to a JSON config file") + + argsparser.add_argument("-s", "--source", dest="source_path", + required=True, metavar="", help="source code path") + + argsparser.add_argument("-p", "--prologue", dest="prologue", + required=False, metavar="", help="path to a C prologue file") + + amalgamation = Amalgamation(argsparser.parse_args()) + amalgamation.generate() + +if __name__ == "__main__": + main() diff --git a/amalgamate/config.json b/amalgamate/config.json new file mode 100644 index 0000000..170950c --- /dev/null +++ b/amalgamate/config.json @@ -0,0 +1,9 @@ +{ + "project": "inja", + "target": "single_include/inja.hpp", + "sources": [ + "../include/inja.hpp" + ], + "include_paths": [ + ] +} diff --git a/include/environment.hpp b/include/environment.hpp new file mode 100644 index 0000000..63148db --- /dev/null +++ b/include/environment.hpp @@ -0,0 +1,130 @@ +#ifndef PANTOR_INJA_ENVIRONMENT_HPP +#define PANTOR_INJA_ENVIRONMENT_HPP + +#include +#include +#include + +#include +#include +#include +#include + + +namespace inja { + +using json = nlohmann::json; + + +/*! +@brief Environment class +*/ +class Environment { + const std::string input_path; + const std::string output_path; + + Parser parser; + Renderer renderer; + +public: + Environment(): Environment("./") { } + explicit Environment(const std::string& global_path): input_path(global_path), output_path(global_path), parser() { } + explicit Environment(const std::string& input_path, const std::string& output_path): input_path(input_path), output_path(output_path), parser() { } + + void set_statement(const std::string& open, const std::string& close) { + parser.regex_map_delimiters[Parsed::Delimiter::Statement] = Regex{open + "\\s*(.+?)\\s*" + close}; + } + + void set_line_statement(const std::string& open) { + parser.regex_map_delimiters[Parsed::Delimiter::LineStatement] = Regex{"(?:^|\\n)" + open + " *(.+?) *(?:\\n|$)"}; + } + + void set_expression(const std::string& open, const std::string& close) { + parser.regex_map_delimiters[Parsed::Delimiter::Expression] = Regex{open + "\\s*(.+?)\\s*" + close}; + } + + void set_comment(const std::string& open, const std::string& close) { + parser.regex_map_delimiters[Parsed::Delimiter::Comment] = Regex{open + "\\s*(.+?)\\s*" + close}; + } + + void set_element_notation(const ElementNotation element_notation_) { + parser.element_notation = element_notation_; + } + + Template parse(const std::string& input) { + return parser.parse(input); + } + + Template parse_template(const std::string& filename) { + return parser.parse_template(input_path + filename); + } + + std::string render(const std::string& input, const json& data) { + return renderer.render(parse(input), data); + } + + std::string render_template(const Template& temp, const json& data) { + return renderer.render(temp, data); + } + + std::string render_file(const std::string& filename, const json& data) { + return renderer.render(parse_template(filename), data); + } + + std::string render_file_with_json_file(const std::string& filename, const std::string& filename_data) { + const json data = load_json(filename_data); + return render_file(filename, data); + } + + void write(const std::string& filename, const json& data, const std::string& filename_out) { + std::ofstream file(output_path + filename_out); + file << render_file(filename, data); + file.close(); + } + + void write(const Template& temp, const json& data, const std::string& filename_out) { + std::ofstream file(output_path + filename_out); + file << render_template(temp, data); + file.close(); + } + + void write_with_json_file(const std::string& filename, const std::string& filename_data, const std::string& filename_out) { + const json data = load_json(filename_data); + write(filename, data, filename_out); + } + + void write_with_json_file(const Template& temp, const std::string& filename_data, const std::string& filename_out) { + const json data = load_json(filename_data); + write(temp, data, filename_out); + } + + std::string load_global_file(const std::string& filename) { + return parser.load_file(input_path + filename); + } + + json load_json(const std::string& filename) { + std::ifstream file(input_path + filename); + json j; + file >> j; + return j; + } + + void add_callback(std::string name, int number_arguments, const std::function& callback) { + const Parsed::CallbackSignature signature = std::make_pair(name, number_arguments); + parser.regex_map_callbacks[signature] = Parser::function_regex(name, number_arguments); + renderer.map_callbacks[signature] = callback; + } + + void include_template(std::string name, const Template& temp) { + parser.included_templates[name] = temp; + } + + template + T get_argument(const Parsed::Arguments& args, int index, const json& data) { + return renderer.eval_expression(args[index], data); + } +}; + +} + +#endif // PANTOR_INJA_ENVIRONMENT_HPP diff --git a/include/error.hpp b/include/error.hpp new file mode 100644 index 0000000..46c9334 --- /dev/null +++ b/include/error.hpp @@ -0,0 +1,18 @@ +#ifndef PANTOR_INJA_ERROR_HPP +#define PANTOR_INJA_ERROR_HPP + +#include + + +namespace inja { + +/*! +@brief throw an error with a given message +*/ +inline void inja_throw(const std::string& type, const std::string& message) { + throw std::runtime_error("[inja.exception." + type + "] " + message); +} + +} + +#endif // PANTOR_INJA_ERROR_HPP diff --git a/include/inja.hpp b/include/inja.hpp new file mode 100644 index 0000000..cc7ab99 --- /dev/null +++ b/include/inja.hpp @@ -0,0 +1,49 @@ +/* +Inja - A Template Engine for Modern C++ +version 1.1.0 +https://github.com/pantor/inja + +Licensed under the MIT License . +Copyright (c) 2017-2018 Pantor . + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef PANTOR_INJA_HPP +#define PANTOR_INJA_HPP + +#define PANTOR_INJA_VERSION_MAJOR 1 +#define PANTOR_INJA_VERSION_MINOR 1 +#define PANTOR_INJA_VERSION_PATCH 0 + + +#include + + +#include "error.hpp" +#include "regex.hpp" +#include "parsed.hpp" +#include "template.hpp" +#include "renderer.hpp" +#include "parser.hpp" +#include "environment.hpp" +#include "utils.hpp" + + +#endif // PANTOR_INJA_HPP diff --git a/src/nlohmann/LICENSE.MIT b/include/nlohmann/LICENSE.MIT similarity index 100% rename from src/nlohmann/LICENSE.MIT rename to include/nlohmann/LICENSE.MIT diff --git a/src/nlohmann/json.hpp b/include/nlohmann/json.hpp similarity index 100% rename from src/nlohmann/json.hpp rename to include/nlohmann/json.hpp diff --git a/include/parsed.hpp b/include/parsed.hpp new file mode 100644 index 0000000..4311209 --- /dev/null +++ b/include/parsed.hpp @@ -0,0 +1,154 @@ +#ifndef PANTOR_INJA_PARSED_HPP +#define PANTOR_INJA_PARSED_HPP + +#include +#include + + +namespace inja { + +using json = nlohmann::json; + + +enum class ElementNotation { + Dot, + Pointer +}; + +struct Parsed { + enum class Type { + Comment, + Condition, + ConditionBranch, + Expression, + Loop, + Main, + String + }; + + enum class Delimiter { + Comment, + Expression, + LineStatement, + Statement + }; + + enum class Statement { + Condition, + Include, + Loop + }; + + enum class Function { + Not, + And, + Or, + In, + Equal, + Greater, + GreaterEqual, + Less, + LessEqual, + Different, + Callback, + DivisibleBy, + Even, + First, + Float, + Int, + Last, + Length, + Lower, + Max, + Min, + Odd, + Range, + Result, + Round, + Sort, + Upper, + ReadJson, + Exists, + ExistsInObject, + IsBoolean, + IsNumber, + IsInteger, + IsFloat, + IsObject, + IsArray, + IsString, + Default + }; + + enum class Condition { + If, + ElseIf, + Else + }; + + enum class Loop { + ForListIn, + ForMapIn + }; + + struct Element { + Type type; + std::string inner; + std::vector> children; + + explicit Element(): Element(Type::Main, "") { } + explicit Element(const Type type): Element(type, "") { } + explicit Element(const Type type, const std::string& inner): type(type), inner(inner), children({}) { } + }; + + struct ElementString: public Element { + const std::string text; + + explicit ElementString(const std::string& text): Element(Type::String), text(text) { } + }; + + struct ElementComment: public Element { + const std::string text; + + explicit ElementComment(const std::string& text): Element(Type::Comment), text(text) { } + }; + + struct ElementExpression: public Element { + Function function; + std::vector args; + std::string command; + json result; + + explicit ElementExpression(): ElementExpression(Function::ReadJson) { } + explicit ElementExpression(const Function function_): Element(Type::Expression), function(function_), args({}), command("") { } + }; + + struct ElementLoop: public Element { + Loop loop; + const std::string key; + const std::string value; + const ElementExpression list; + + explicit ElementLoop(const Loop loop_, const std::string& value, const ElementExpression& list, const std::string& inner): Element(Type::Loop, inner), loop(loop_), value(value), list(list) { } + explicit ElementLoop(const Loop loop_, const std::string& key, const std::string& value, const ElementExpression& list, const std::string& inner): Element(Type::Loop, inner), loop(loop_), key(key), value(value), list(list) { } + }; + + struct ElementConditionContainer: public Element { + explicit ElementConditionContainer(): Element(Type::Condition) { } + }; + + struct ElementConditionBranch: public Element { + const Condition condition_type; + const ElementExpression condition; + + explicit ElementConditionBranch(const std::string& inner, const Condition condition_type): Element(Type::ConditionBranch, inner), condition_type(condition_type) { } + explicit ElementConditionBranch(const std::string& inner, const Condition condition_type, const ElementExpression& condition): Element(Type::ConditionBranch, inner), condition_type(condition_type), condition(condition) { } + }; + + using Arguments = std::vector; + using CallbackSignature = std::pair; +}; + +} + +#endif // PANTOR_INJA_PARSED_HPP diff --git a/include/parser.hpp b/include/parser.hpp new file mode 100644 index 0000000..9d9e604 --- /dev/null +++ b/include/parser.hpp @@ -0,0 +1,345 @@ +#ifndef PANTOR_INJA_PARSER_HPP +#define PANTOR_INJA_PARSER_HPP + +#include +#include +#include +#include + +#include +#include + + +namespace inja { + +using json = nlohmann::json; + + +class Parser { +public: + ElementNotation element_notation = ElementNotation::Pointer; + + std::map> regex_map_callbacks; + + std::map included_templates; + + /*! + @brief create a corresponding regex for a function name with a number of arguments separated by , + */ + static Regex function_regex(const std::string& name, int number_arguments) { + std::string pattern = name; + pattern.append("(?:\\("); + for (int i = 0; i < number_arguments; i++) { + if (i != 0) pattern.append(","); + pattern.append("(.*)"); + } + pattern.append("\\))"); + if (number_arguments == 0) { // Without arguments, allow to use the callback without parenthesis + pattern.append("?"); + } + return Regex{"\\s*" + pattern + "\\s*"}; + } + + /*! + @brief dot notation to json pointer notation + */ + static std::string dot_to_json_pointer_notation(const std::string& dot) { + std::string result = dot; + while (result.find(".") != std::string::npos) { + result.replace(result.find("."), 1, "/"); + } + result.insert(0, "/"); + return result; + } + + std::map regex_map_delimiters = { + {Parsed::Delimiter::Statement, Regex{"\\{\\%\\s*(.+?)\\s*\\%\\}"}}, + {Parsed::Delimiter::LineStatement, Regex{"(?:^|\\n)## *(.+?) *(?:\\n|$)"}}, + {Parsed::Delimiter::Expression, Regex{"\\{\\{\\s*(.+?)\\s*\\}\\}"}}, + {Parsed::Delimiter::Comment, Regex{"\\{#\\s*(.*?)\\s*#\\}"}} + }; + + const std::map regex_map_statement_openers = { + {Parsed::Statement::Loop, Regex{"for\\s+(.+)"}}, + {Parsed::Statement::Condition, Regex{"if\\s+(.+)"}}, + {Parsed::Statement::Include, Regex{"include\\s+\"(.+)\""}} + }; + + const std::map regex_map_statement_closers = { + {Parsed::Statement::Loop, Regex{"endfor"}}, + {Parsed::Statement::Condition, Regex{"endif"}} + }; + + const std::map regex_map_loop = { + {Parsed::Loop::ForListIn, Regex{"for\\s+(\\w+)\\s+in\\s+(.+)"}}, + {Parsed::Loop::ForMapIn, Regex{"for\\s+(\\w+),\\s+(\\w+)\\s+in\\s+(.+)"}}, + }; + + const std::map regex_map_condition = { + {Parsed::Condition::If, Regex{"if\\s+(.+)"}}, + {Parsed::Condition::ElseIf, Regex{"else\\s+if\\s+(.+)"}}, + {Parsed::Condition::Else, Regex{"else"}} + }; + + const std::map regex_map_functions = { + {Parsed::Function::Not, Regex{"not (.+)"}}, + {Parsed::Function::And, Regex{"(.+) and (.+)"}}, + {Parsed::Function::Or, Regex{"(.+) or (.+)"}}, + {Parsed::Function::In, Regex{"(.+) in (.+)"}}, + {Parsed::Function::Equal, Regex{"(.+) == (.+)"}}, + {Parsed::Function::Greater, Regex{"(.+) > (.+)"}}, + {Parsed::Function::Less, Regex{"(.+) < (.+)"}}, + {Parsed::Function::GreaterEqual, Regex{"(.+) >= (.+)"}}, + {Parsed::Function::LessEqual, Regex{"(.+) <= (.+)"}}, + {Parsed::Function::Different, Regex{"(.+) != (.+)"}}, + {Parsed::Function::Default, function_regex("default", 2)}, + {Parsed::Function::DivisibleBy, function_regex("divisibleBy", 2)}, + {Parsed::Function::Even, function_regex("even", 1)}, + {Parsed::Function::First, function_regex("first", 1)}, + {Parsed::Function::Float, function_regex("float", 1)}, + {Parsed::Function::Int, function_regex("int", 1)}, + {Parsed::Function::Last, function_regex("last", 1)}, + {Parsed::Function::Length, function_regex("length", 1)}, + {Parsed::Function::Lower, function_regex("lower", 1)}, + {Parsed::Function::Max, function_regex("max", 1)}, + {Parsed::Function::Min, function_regex("min", 1)}, + {Parsed::Function::Odd, function_regex("odd", 1)}, + {Parsed::Function::Range, function_regex("range", 1)}, + {Parsed::Function::Round, function_regex("round", 2)}, + {Parsed::Function::Sort, function_regex("sort", 1)}, + {Parsed::Function::Upper, function_regex("upper", 1)}, + {Parsed::Function::Exists, function_regex("exists", 1)}, + {Parsed::Function::ExistsInObject, function_regex("existsIn", 2)}, + {Parsed::Function::IsBoolean, function_regex("isBoolean", 1)}, + {Parsed::Function::IsNumber, function_regex("isNumber", 1)}, + {Parsed::Function::IsInteger, function_regex("isInteger", 1)}, + {Parsed::Function::IsFloat, function_regex("isFloat", 1)}, + {Parsed::Function::IsObject, function_regex("isObject", 1)}, + {Parsed::Function::IsArray, function_regex("isArray", 1)}, + {Parsed::Function::IsString, function_regex("isString", 1)}, + {Parsed::Function::ReadJson, Regex{"\\s*([^\\(\\)]*\\S)\\s*"}} + }; + + Parser() { } + + Parsed::ElementExpression parse_expression(const std::string& input) { + const MatchType match_callback = match(input, regex_map_callbacks); + if (!match_callback.type().first.empty()) { + std::vector args {}; + for (unsigned int i = 1; i < match_callback.size(); i++) { // str(0) is whole group + args.push_back( parse_expression(match_callback.str(i)) ); + } + + Parsed::ElementExpression result = Parsed::ElementExpression(Parsed::Function::Callback); + result.args = args; + result.command = match_callback.type().first; + return result; + } + + const MatchType match_function = match(input, regex_map_functions); + switch ( match_function.type() ) { + case Parsed::Function::ReadJson: { + std::string command = match_function.str(1); + if ( json::accept(command) ) { // JSON Result + Parsed::ElementExpression result = Parsed::ElementExpression(Parsed::Function::Result); + result.result = json::parse(command); + return result; + } + + Parsed::ElementExpression result = Parsed::ElementExpression(Parsed::Function::ReadJson); + switch (element_notation) { + case ElementNotation::Pointer: { + if (command[0] != '/') { command.insert(0, "/"); } + result.command = command; + break; + } + case ElementNotation::Dot: { + result.command = dot_to_json_pointer_notation(command); + break; + } + } + return result; + } + default: { + std::vector args = {}; + for (unsigned int i = 1; i < match_function.size(); i++) { // str(0) is whole group + args.push_back( parse_expression(match_function.str(i)) ); + } + + Parsed::ElementExpression result = Parsed::ElementExpression(match_function.type()); + result.args = args; + return result; + } + } + } + + std::vector> parse_level(const std::string& input, const std::string& path) { + std::vector> result; + + size_t current_position = 0; + MatchType match_delimiter = search(input, regex_map_delimiters, current_position); + while (match_delimiter.found()) { + current_position = match_delimiter.end_position(); + const std::string string_prefix = match_delimiter.prefix(); + if (not string_prefix.empty()) { + result.emplace_back( std::make_shared(string_prefix) ); + } + + const std::string delimiter_inner = match_delimiter.str(1); + + switch ( match_delimiter.type() ) { + case Parsed::Delimiter::Statement: + case Parsed::Delimiter::LineStatement: { + + const MatchType match_statement = match(delimiter_inner, regex_map_statement_openers); + switch ( match_statement.type() ) { + case Parsed::Statement::Loop: { + const MatchClosed loop_match = search_closed(input, match_delimiter.regex(), regex_map_statement_openers.at(Parsed::Statement::Loop), regex_map_statement_closers.at(Parsed::Statement::Loop), match_delimiter); + + current_position = loop_match.end_position(); + + const std::string loop_inner = match_statement.str(0); + const MatchType match_command = match(loop_inner, regex_map_loop); + if (not match_command.found()) { + inja_throw("parser_error", "unknown loop statement: " + loop_inner); + } + switch (match_command.type()) { + case Parsed::Loop::ForListIn: { + const std::string value_name = match_command.str(1); + const std::string list_name = match_command.str(2); + + result.emplace_back( std::make_shared(match_command.type(), value_name, parse_expression(list_name), loop_match.inner())); + break; + } + case Parsed::Loop::ForMapIn: { + const std::string key_name = match_command.str(1); + const std::string value_name = match_command.str(2); + const std::string list_name = match_command.str(3); + + result.emplace_back( std::make_shared(match_command.type(), key_name, value_name, parse_expression(list_name), loop_match.inner())); + break; + } + } + break; + } + case Parsed::Statement::Condition: { + auto condition_container = std::make_shared(); + + Match condition_match = match_delimiter; + MatchClosed else_if_match = search_closed_on_level(input, match_delimiter.regex(), regex_map_statement_openers.at(Parsed::Statement::Condition), regex_map_statement_closers.at(Parsed::Statement::Condition), regex_map_condition.at(Parsed::Condition::ElseIf), condition_match); + while (else_if_match.found()) { + condition_match = else_if_match.close_match; + + const std::string else_if_match_inner = else_if_match.open_match.str(1); + const MatchType match_command = match(else_if_match_inner, regex_map_condition); + if (not match_command.found()) { + inja_throw("parser_error", "unknown if statement: " + else_if_match.open_match.str()); + } + condition_container->children.push_back( std::make_shared(else_if_match.inner(), match_command.type(), parse_expression(match_command.str(1))) ); + + else_if_match = search_closed_on_level(input, match_delimiter.regex(), regex_map_statement_openers.at(Parsed::Statement::Condition), regex_map_statement_closers.at(Parsed::Statement::Condition), regex_map_condition.at(Parsed::Condition::ElseIf), condition_match); + } + + MatchClosed else_match = search_closed_on_level(input, match_delimiter.regex(), regex_map_statement_openers.at(Parsed::Statement::Condition), regex_map_statement_closers.at(Parsed::Statement::Condition), regex_map_condition.at(Parsed::Condition::Else), condition_match); + if (else_match.found()) { + condition_match = else_match.close_match; + + const std::string else_match_inner = else_match.open_match.str(1); + const MatchType match_command = match(else_match_inner, regex_map_condition); + if (not match_command.found()) { + inja_throw("parser_error", "unknown if statement: " + else_match.open_match.str()); + } + condition_container->children.push_back( std::make_shared(else_match.inner(), match_command.type(), parse_expression(match_command.str(1))) ); + } + + const MatchClosed last_if_match = search_closed(input, match_delimiter.regex(), regex_map_statement_openers.at(Parsed::Statement::Condition), regex_map_statement_closers.at(Parsed::Statement::Condition), condition_match); + if (not last_if_match.found()) { + inja_throw("parser_error", "misordered if statement"); + } + + const std::string last_if_match_inner = last_if_match.open_match.str(1); + const MatchType match_command = match(last_if_match_inner, regex_map_condition); + if (not match_command.found()) { + inja_throw("parser_error", "unknown if statement: " + last_if_match.open_match.str()); + } + if (match_command.type() == Parsed::Condition::Else) { + condition_container->children.push_back( std::make_shared(last_if_match.inner(), match_command.type()) ); + } else { + condition_container->children.push_back( std::make_shared(last_if_match.inner(), match_command.type(), parse_expression(match_command.str(1))) ); + } + + current_position = last_if_match.end_position(); + result.emplace_back(condition_container); + break; + } + case Parsed::Statement::Include: { + const std::string template_name = match_statement.str(1); + Template included_template; + if (included_templates.find( template_name ) != included_templates.end()) { + included_template = included_templates[template_name]; + } else { + included_template = parse_template(path + template_name); + } + + auto children = included_template.parsed_template().children; + result.insert(result.end(), children.begin(), children.end()); + break; + } + } + break; + } + case Parsed::Delimiter::Expression: { + result.emplace_back( std::make_shared(parse_expression(delimiter_inner)) ); + break; + } + case Parsed::Delimiter::Comment: { + result.emplace_back( std::make_shared(delimiter_inner) ); + break; + } + } + + match_delimiter = search(input, regex_map_delimiters, current_position); + } + if (current_position < input.length()) { + result.emplace_back( std::make_shared(input.substr(current_position)) ); + } + + return result; + } + + std::shared_ptr parse_tree(std::shared_ptr current_element, const std::string& path) { + if (not current_element->inner.empty()) { + current_element->children = parse_level(current_element->inner, path); + current_element->inner.clear(); + } + + if (not current_element->children.empty()) { + for (auto& child: current_element->children) { + child = parse_tree(child, path); + } + } + return current_element; + } + + Template parse(const std::string& input) { + auto parsed = parse_tree(std::make_shared(Parsed::Element(Parsed::Type::Main, input)), "./"); + return Template(*parsed); + } + + Template parse_template(const std::string& filename) { + const std::string input = load_file(filename); + const std::string path = filename.substr(0, filename.find_last_of("/\\") + 1); + auto parsed = parse_tree(std::make_shared(Parsed::Element(Parsed::Type::Main, input)), path); + return Template(*parsed); + } + + std::string load_file(const std::string& filename) { + std::ifstream file(filename); + std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + return text; + } +}; + +} + +#endif // PANTOR_INJA_PARSER_HPP diff --git a/include/regex.hpp b/include/regex.hpp new file mode 100644 index 0000000..baa146c --- /dev/null +++ b/include/regex.hpp @@ -0,0 +1,171 @@ +#ifndef PANTOR_INJA_REGEX_HPP +#define PANTOR_INJA_REGEX_HPP + +#include +#include +#include + + +namespace inja { + +/*! +@brief inja regex class, saves string pattern in addition to std::regex +*/ +class Regex: public std::regex { + std::string pattern_; + +public: + Regex(): std::regex() {} + explicit Regex(const std::string& pattern): std::regex(pattern, std::regex_constants::ECMAScript), pattern_(pattern) { } + + std::string pattern() const { return pattern_; } +}; + + +class Match: public std::match_results { + size_t offset_ {0}; + unsigned int group_offset_ {0}; + Regex regex_; + +public: + Match(): std::match_results() { } + explicit Match(size_t offset): std::match_results(), offset_(offset) { } + explicit Match(size_t offset, const Regex& regex): std::match_results(), offset_(offset), regex_(regex) { } + + void set_group_offset(unsigned int group_offset) { group_offset_ = group_offset; } + void set_regex(Regex regex) { regex_ = regex; } + + size_t position() const { return offset_ + std::match_results::position(); } + size_t end_position() const { return position() + length(); } + bool found() const { return not empty(); } + const std::string str() const { return str(0); } + const std::string str(int i) const { return std::match_results::str(i + group_offset_); } + Regex regex() const { return regex_; } +}; + + +template +class MatchType: public Match { + T type_; + +public: + MatchType(): Match() { } + explicit MatchType(const Match& obj): Match(obj) { } + MatchType(Match&& obj): Match(std::move(obj)) { } + + void set_type(T type) { type_ = type; } + + T type() const { return type_; } +}; + + +class MatchClosed { +public: + Match open_match, close_match; + + MatchClosed() { } + MatchClosed(Match& open_match, Match& close_match): open_match(open_match), close_match(close_match) { } + + size_t position() const { return open_match.position(); } + size_t end_position() const { return close_match.end_position(); } + size_t length() const { return close_match.end_position() - open_match.position(); } + bool found() const { return open_match.found() and close_match.found(); } + std::string prefix() const { return open_match.prefix().str(); } + std::string suffix() const { return close_match.suffix().str(); } + std::string outer() const { return open_match.str() + static_cast(open_match.suffix()).substr(0, close_match.end_position() - open_match.end_position()); } + std::string inner() const { return static_cast(open_match.suffix()).substr(0, close_match.position() - open_match.end_position()); } +}; + + +inline Match search(const std::string& input, const Regex& regex, size_t position) { + if (position >= input.length()) { return Match(); } + + Match match{position, regex}; + std::regex_search(input.cbegin() + position, input.cend(), match, regex); + return match; +} + + +template +inline MatchType search(const std::string& input, const std::map& regexes, size_t position) { + // Map to vectors + std::vector class_vector; + std::vector regexes_vector; + for (const auto& element: regexes) { + class_vector.push_back(element.first); + regexes_vector.push_back(element.second); + } + + // Regex join + std::stringstream ss; + for (size_t i = 0; i < regexes_vector.size(); ++i) + { + if (i != 0) { ss << ")|("; } + ss << regexes_vector[i].pattern(); + } + Regex regex{"(" + ss.str() + ")"}; + + MatchType search_match = search(input, regex, position); + if (not search_match.found()) { return MatchType(); } + + // Vector of id vs groups + std::vector regex_mark_counts = {}; + for (unsigned int i = 0; i < regexes_vector.size(); i++) { + for (unsigned int j = 0; j < regexes_vector[i].mark_count() + 1; j++) { + regex_mark_counts.push_back(i); + } + } + + for (unsigned int i = 1; i < search_match.size(); i++) { + if (search_match.length(i) > 0) { + search_match.set_group_offset(i); + search_match.set_type(class_vector[regex_mark_counts[i]]); + search_match.set_regex(regexes_vector[regex_mark_counts[i]]); + return search_match; + } + } + + inja_throw("regex_search_error", "error while searching in input: " + input); + return search_match; +} + +inline MatchClosed search_closed_on_level(const std::string& input, const Regex& regex_statement, const Regex& regex_level_up, const Regex& regex_level_down, const Regex& regex_search, Match open_match) { + + int level {0}; + size_t current_position = open_match.end_position(); + Match match_delimiter = search(input, regex_statement, current_position); + while (match_delimiter.found()) { + current_position = match_delimiter.end_position(); + + const std::string inner = match_delimiter.str(1); + if (std::regex_match(inner.cbegin(), inner.cend(), regex_search) and level == 0) { break; } + if (std::regex_match(inner.cbegin(), inner.cend(), regex_level_up)) { level += 1; } + else if (std::regex_match(inner.cbegin(), inner.cend(), regex_level_down)) { level -= 1; } + + if (level < 0) { return MatchClosed(); } + match_delimiter = search(input, regex_statement, current_position); + } + + return MatchClosed(open_match, match_delimiter); +} + +inline MatchClosed search_closed(const std::string& input, const Regex& regex_statement, const Regex& regex_open, const Regex& regex_close, Match& open_match) { + return search_closed_on_level(input, regex_statement, regex_open, regex_close, regex_close, open_match); +} + +template +inline MatchType match(const std::string& input, const std::map& regexes) { + MatchType match; + for (const auto& e: regexes) { + if (std::regex_match(input.cbegin(), input.cend(), match, e.second)) { + match.set_type(e.first); + match.set_regex(e.second); + return match; + } + } + return match; +} + +} + +#endif // PANTOR_INJA_REGEX_HPP diff --git a/include/renderer.hpp b/include/renderer.hpp new file mode 100644 index 0000000..76250a0 --- /dev/null +++ b/include/renderer.hpp @@ -0,0 +1,285 @@ +#ifndef PANTOR_INJA_RENDERER_HPP +#define PANTOR_INJA_RENDERER_HPP + +#include +#include +#include + + +namespace inja { + +using json = nlohmann::json; + + +class Renderer { +public: + std::map> map_callbacks; + + template + bool eval_expression(const Parsed::ElementExpression& element, const json& data) { + const json var = eval_function(element, data); + if (var.empty()) { return false; } + else if (var.is_number()) { return (var != 0); } + else if (var.is_string()) { return not var.empty(); } + try { + return var.get(); + } catch (json::type_error& e) { + inja_throw("json_error", e.what()); + throw; + } + } + + template + T eval_expression(const Parsed::ElementExpression& element, const json& data) { + const json var = eval_function(element, data); + if (var.empty()) return T(); + try { + return var.get(); + } catch (json::type_error& e) { + inja_throw("json_error", e.what()); + throw; + } + } + + json eval_function(const Parsed::ElementExpression& element, const json& data) { + switch (element.function) { + case Parsed::Function::Upper: { + std::string str = eval_expression(element.args[0], data); + std::transform(str.begin(), str.end(), str.begin(), ::toupper); + return str; + } + case Parsed::Function::Lower: { + std::string str = eval_expression(element.args[0], data); + std::transform(str.begin(), str.end(), str.begin(), ::tolower); + return str; + } + case Parsed::Function::Range: { + const int number = eval_expression(element.args[0], data); + std::vector result(number); + std::iota(std::begin(result), std::end(result), 0); + return result; + } + case Parsed::Function::Length: { + const std::vector list = eval_expression>(element.args[0], data); + return list.size(); + } + case Parsed::Function::Sort: { + std::vector list = eval_expression>(element.args[0], data); + std::sort(list.begin(), list.end()); + return list; + } + case Parsed::Function::First: { + const std::vector list = eval_expression>(element.args[0], data); + return list.front(); + } + case Parsed::Function::Last: { + const std::vector list = eval_expression>(element.args[0], data); + return list.back(); + } + case Parsed::Function::Round: { + const double number = eval_expression(element.args[0], data); + const int precision = eval_expression(element.args[1], data); + return std::round(number * std::pow(10.0, precision)) / std::pow(10.0, precision); + } + case Parsed::Function::DivisibleBy: { + const int number = eval_expression(element.args[0], data); + const int divisor = eval_expression(element.args[1], data); + return (divisor != 0) && (number % divisor == 0); + } + case Parsed::Function::Odd: { + const int number = eval_expression(element.args[0], data); + return (number % 2 != 0); + } + case Parsed::Function::Even: { + const int number = eval_expression(element.args[0], data); + return (number % 2 == 0); + } + case Parsed::Function::Max: { + const std::vector list = eval_expression>(element.args[0], data); + return *std::max_element(list.begin(), list.end()); + } + case Parsed::Function::Min: { + const std::vector list = eval_expression>(element.args[0], data); + return *std::min_element(list.begin(), list.end()); + } + case Parsed::Function::Not: { + return not eval_expression(element.args[0], data); + } + case Parsed::Function::And: { + return (eval_expression(element.args[0], data) and eval_expression(element.args[1], data)); + } + case Parsed::Function::Or: { + return (eval_expression(element.args[0], data) or eval_expression(element.args[1], data)); + } + case Parsed::Function::In: { + const json value = eval_expression(element.args[0], data); + const json list = eval_expression(element.args[1], data); + return (std::find(list.begin(), list.end(), value) != list.end()); + } + case Parsed::Function::Equal: { + return eval_expression(element.args[0], data) == eval_expression(element.args[1], data); + } + case Parsed::Function::Greater: { + return eval_expression(element.args[0], data) > eval_expression(element.args[1], data); + } + case Parsed::Function::Less: { + return eval_expression(element.args[0], data) < eval_expression(element.args[1], data); + } + case Parsed::Function::GreaterEqual: { + return eval_expression(element.args[0], data) >= eval_expression(element.args[1], data); + } + case Parsed::Function::LessEqual: { + return eval_expression(element.args[0], data) <= eval_expression(element.args[1], data); + } + case Parsed::Function::Different: { + return eval_expression(element.args[0], data) != eval_expression(element.args[1], data); + } + case Parsed::Function::Float: { + return std::stod(eval_expression(element.args[0], data)); + } + case Parsed::Function::Int: { + return std::stoi(eval_expression(element.args[0], data)); + } + case Parsed::Function::ReadJson: { + try { + return data.at(json::json_pointer(element.command)); + } catch (std::exception&) { + inja_throw("render_error", "variable '" + element.command + "' not found"); + } + } + case Parsed::Function::Result: { + return element.result; + } + case Parsed::Function::Default: { + try { + return eval_expression(element.args[0], data); + } catch (std::exception&) { + return eval_expression(element.args[1], data); + } + } + case Parsed::Function::Callback: { + Parsed::CallbackSignature signature = std::make_pair(element.command, element.args.size()); + return map_callbacks.at(signature)(element.args, data); + } + case Parsed::Function::Exists: { + const std::string name = eval_expression(element.args[0], data); + return data.find(name) != data.end(); + } + case Parsed::Function::ExistsInObject: { + const std::string name = eval_expression(element.args[1], data); + const json d = eval_expression(element.args[0], data); + return d.find(name) != d.end(); + } + case Parsed::Function::IsBoolean: { + const json d = eval_expression(element.args[0], data); + return d.is_boolean(); + } + case Parsed::Function::IsNumber: { + const json d = eval_expression(element.args[0], data); + return d.is_number(); + } + case Parsed::Function::IsInteger: { + const json d = eval_expression(element.args[0], data); + return d.is_number_integer(); + } + case Parsed::Function::IsFloat: { + const json d = eval_expression(element.args[0], data); + return d.is_number_float(); + } + case Parsed::Function::IsObject: { + const json d = eval_expression(element.args[0], data); + return d.is_object(); + } + case Parsed::Function::IsArray: { + const json d = eval_expression(element.args[0], data); + return d.is_array(); + } + case Parsed::Function::IsString: { + const json d = eval_expression(element.args[0], data); + return d.is_string(); + } + } + + inja_throw("render_error", "unknown function in renderer: " + element.command); + return json(); + } + + std::string render(Template temp, const json& data) { + std::string result {""}; + for (const auto& element: temp.parsed_template().children) { + switch (element->type) { + case Parsed::Type::String: { + auto element_string = std::static_pointer_cast(element); + result.append(element_string->text); + break; + } + case Parsed::Type::Expression: { + auto element_expression = std::static_pointer_cast(element); + const json variable = eval_expression(*element_expression, data); + + if (variable.is_string()) { + result.append( variable.get() ); + } else { + std::stringstream ss; + ss << variable; + result.append( ss.str() ); + } + break; + } + case Parsed::Type::Loop: { + auto element_loop = std::static_pointer_cast(element); + switch (element_loop->loop) { + case Parsed::Loop::ForListIn: { + const std::vector list = eval_expression>(element_loop->list, data); + for (unsigned int i = 0; i < list.size(); i++) { + json data_loop = data; + /* For nested loops, use parent/index */ + if (data_loop.count("loop") == 1) { + data_loop["loop"]["parent"] = data_loop["loop"]; + } + data_loop[element_loop->value] = list[i]; + data_loop["loop"]["index"] = i; + data_loop["loop"]["index1"] = i + 1; + data_loop["loop"]["is_first"] = (i == 0); + data_loop["loop"]["is_last"] = (i == list.size() - 1); + result.append( render(Template(*element_loop), data_loop) ); + } + break; + } + case Parsed::Loop::ForMapIn: { + const std::map map = eval_expression>(element_loop->list, data); + for (const auto& item: map) { + json data_loop = data; + data_loop[element_loop->key] = item.first; + data_loop[element_loop->value] = item.second; + result.append( render(Template(*element_loop), data_loop) ); + } + break; + } + } + + break; + } + case Parsed::Type::Condition: { + auto element_condition = std::static_pointer_cast(element); + for (const auto& branch: element_condition->children) { + auto element_branch = std::static_pointer_cast(branch); + if (element_branch->condition_type == Parsed::Condition::Else || eval_expression(element_branch->condition, data)) { + result.append( render(Template(*element_branch), data) ); + break; + } + } + break; + } + default: { + break; + } + } + } + return result; + } +}; + +} + +#endif // PANTOR_INJA_RENDERER_HPP diff --git a/include/template.hpp b/include/template.hpp new file mode 100644 index 0000000..87b4a1c --- /dev/null +++ b/include/template.hpp @@ -0,0 +1,19 @@ +#ifndef PANTOR_INJA_TEMPLATE_HPP +#define PANTOR_INJA_TEMPLATE_HPP + + +namespace inja { + +class Template { + Parsed::Element _parsed_template; + +public: + const Parsed::Element parsed_template() { return _parsed_template; } + + explicit Template(): _parsed_template(Parsed::Element()) { } + explicit Template(const Parsed::Element& parsed_template): _parsed_template(parsed_template) { } +}; + +} + +#endif // PANTOR_INJA_TEMPLATE_HPP diff --git a/include/utils.hpp b/include/utils.hpp new file mode 100644 index 0000000..1e6a9dc --- /dev/null +++ b/include/utils.hpp @@ -0,0 +1,16 @@ +#ifndef PANTOR_INJA_UTILS_HPP +#define PANTOR_INJA_UTILS_HPP + +#include + + +namespace inja { + /*! + @brief render with default settings + */ + inline std::string render(const std::string& input, const json& data) { + return Environment().render(input, data); + } +} + +#endif // PANTOR_INJA_UTILS_HPP diff --git a/meson.build b/meson.build index 3aff495..282dbba 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,22 @@ -project('inja', 'cpp', default_options : ['cpp_std=c++11']) +project('inja', 'cpp', default_options: ['cpp_std=c++11']) + inja_dep = declare_dependency( - include_directories : include_directories('src') + include_directories: include_directories('include') ) +inja_single_dep = declare_dependency( + include_directories: include_directories('single_include', 'include') +) + + +# Amalgamate inja header files +r = run_command('python3', 'amalgamate/amalgamate.py', '-c', 'amalgamate/config.json', '-s', 'include') +if r.returncode() != 0 + message(r.stdout().strip()) +else + message('Amalgamated inja header files.') +endif + + subdir('test') diff --git a/src/inja.hpp b/single_include/inja.hpp similarity index 95% rename from src/inja.hpp rename to single_include/inja.hpp index a278a63..87f2ae9 100644 --- a/src/inja.hpp +++ b/single_include/inja.hpp @@ -33,24 +33,18 @@ SOFTWARE. #define PANTOR_INJA_VERSION_PATCH 0 -#include -#include -#include -#include -#include #include -#include + + +// #include "error.hpp" +#ifndef PANTOR_INJA_ERROR_HPP +#define PANTOR_INJA_ERROR_HPP + #include -#include -#include -#include namespace inja { -using json = nlohmann::json; - - /*! @brief throw an error with a given message */ @@ -58,6 +52,20 @@ inline void inja_throw(const std::string& type, const std::string& message) { throw std::runtime_error("[inja.exception." + type + "] " + message); } +} + +#endif // PANTOR_INJA_ERROR_HPP + +// #include "regex.hpp" +#ifndef PANTOR_INJA_REGEX_HPP +#define PANTOR_INJA_REGEX_HPP + +#include +#include +#include + + +namespace inja { /*! @brief inja regex class, saves string pattern in addition to std::regex @@ -217,6 +225,22 @@ inline MatchType match(const std::string& input, const std::map& return match; } +} + +#endif // PANTOR_INJA_REGEX_HPP + +// #include "parsed.hpp" +#ifndef PANTOR_INJA_PARSED_HPP +#define PANTOR_INJA_PARSED_HPP + +#include +#include + + +namespace inja { + +using json = nlohmann::json; + enum class ElementNotation { Dot, @@ -357,6 +381,16 @@ struct Parsed { using CallbackSignature = std::pair; }; +} + +#endif // PANTOR_INJA_PARSED_HPP + +// #include "template.hpp" +#ifndef PANTOR_INJA_TEMPLATE_HPP +#define PANTOR_INJA_TEMPLATE_HPP + + +namespace inja { class Template { Parsed::Element _parsed_template; @@ -368,6 +402,23 @@ public: explicit Template(const Parsed::Element& parsed_template): _parsed_template(parsed_template) { } }; +} + +#endif // PANTOR_INJA_TEMPLATE_HPP + +// #include "renderer.hpp" +#ifndef PANTOR_INJA_RENDERER_HPP +#define PANTOR_INJA_RENDERER_HPP + +#include +#include +#include + + +namespace inja { + +using json = nlohmann::json; + class Renderer { public: @@ -638,6 +689,27 @@ public: } }; +} + +#endif // PANTOR_INJA_RENDERER_HPP + +// #include "parser.hpp" +#ifndef PANTOR_INJA_PARSER_HPP +#define PANTOR_INJA_PARSER_HPP + +#include +#include +#include +#include + +#include +#include + + +namespace inja { + +using json = nlohmann::json; + class Parser { public: @@ -964,6 +1036,27 @@ public: } }; +} + +#endif // PANTOR_INJA_PARSER_HPP + +// #include "environment.hpp" +#ifndef PANTOR_INJA_ENVIRONMENT_HPP +#define PANTOR_INJA_ENVIRONMENT_HPP + +#include +#include +#include + +#include +#include +#include + + +namespace inja { + +using json = nlohmann::json; + /*! @brief Environment class @@ -1074,14 +1167,28 @@ public: } }; - -/*! -@brief render with default settings -*/ -inline std::string render(const std::string& input, const json& data) { - return Environment().render(input, data); } -} // namespace inja +#endif // PANTOR_INJA_ENVIRONMENT_HPP + +// #include "utils.hpp" +#ifndef PANTOR_INJA_UTILS_HPP +#define PANTOR_INJA_UTILS_HPP + +#include + + +namespace inja { + /*! + @brief render with default settings + */ + inline std::string render(const std::string& input, const json& data) { + return Environment().render(input, data); + } +} + +#endif // PANTOR_INJA_UTILS_HPP + + #endif // PANTOR_INJA_HPP diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2608f76..c90197a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -21,6 +21,13 @@ add_executable(inja_test src/unit.cpp ) +add_executable(inja_single_test + src/unit-files.cpp + src/unit-renderer.cpp + src/unit-string-helper.cpp + src/unit.cpp +) + add_executable(inja_benchmark src/benchmark.cpp ) @@ -51,6 +58,7 @@ else() # Manage dependencies manually # Add dependencies to targets target_link_libraries(inja_test Catch inja) + target_link_libraries(inja_single_test Catch inja_single) target_link_libraries(inja_benchmark hayai inja) endif() @@ -73,3 +81,8 @@ add_test(NAME inja_test COMMAND inja_test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) + +add_test(NAME inja_single_test + COMMAND inja_single_test + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/test/meson.build b/test/meson.build index e3bb23e..938ce69 100644 --- a/test/meson.build +++ b/test/meson.build @@ -7,6 +7,15 @@ unit_test = executable( dependencies: inja_dep ) +unit_single_test = executable( + 'inja-single-test', + 'src/unit.cpp', + 'src/unit-files.cpp', + 'src/unit-renderer.cpp', + 'src/unit-string-helper.cpp', + dependencies: inja_single_dep +) + inja_benchmark = executable( 'inja_benchmark', 'src/benchmark.cpp', @@ -14,3 +23,4 @@ inja_benchmark = executable( ) test('Inja unit test', unit_test) +test('Inja single unit test', unit_single_test)