Mercurial > embed
changeset 81:7fdaf1411e6f
mustache: import 3.2.1, closes #1714
author | David Demelier <markand@malikania.fr> |
---|---|
date | Thu, 25 Jul 2019 20:30:00 +0000 |
parents | 7bb5167cffb4 |
children | 3c05ebba5cb8 |
files | CMakeLists.txt LICENSE.libmustache.txt VERSION.libmustache.txt libmustache/CMakeLists.txt libmustache/mustache.hpp tests/libmustache/CMakeLists.txt tests/libmustache/main.cpp |
diffstat | 7 files changed, 1217 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Thu Jul 25 20:25:00 2019 +0000 +++ b/CMakeLists.txt Thu Jul 25 20:30:00 2019 +0000 @@ -29,6 +29,7 @@ add_subdirectory(tests/libhoedown) add_subdirectory(tests/libjansson) add_subdirectory(tests/libjson) +add_subdirectory(tests/libmustache) add_subdirectory(tests/libpugixml) add_subdirectory(tests/libsqlite) add_subdirectory(tests/liburiparser)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE.libmustache.txt Thu Jul 25 20:30:00 2019 +0000 @@ -0,0 +1,25 @@ +Boost Software License - Version 1.0 + +Copyright 2015-2018 Kevin Wojniak + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VERSION.libmustache.txt Thu Jul 25 20:30:00 2019 +0000 @@ -0,0 +1,1 @@ +3.2.1
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libmustache/CMakeLists.txt Thu Jul 25 20:30:00 2019 +0000 @@ -0,0 +1,27 @@ +# +# CMakeLists.txt -- CMake build system for kainjow/mustache +# +# Copyright (c) 2016-2018 David Demelier <markand@malikania.fr> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +cmake_minimum_required(VERSION 3.0) +project(libmustache) +add_library(libmustache INTERFACE) +target_include_directories( + libmustache + INTERFACE + $<BUILD_INTERFACE:${libmustache_SOURCE_DIR}> +) +target_sources(libmustache INTERFACE ${libmustache_SOURCE_DIR}/mustache.hpp)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libmustache/mustache.hpp Thu Jul 25 20:30:00 2019 +0000 @@ -0,0 +1,1112 @@ +/* + * Boost Software License - Version 1.0 + * + * Copyright 2015-2018 Kevin Wojniak + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef KAINJOW_MUSTACHE_HPP +#define KAINJOW_MUSTACHE_HPP + +#include <cassert> +#include <functional> +#include <iostream> +#include <memory> +#include <sstream> +#include <unordered_map> +#include <vector> + +namespace kainjow { +namespace mustache { + +template <typename string_type> +string_type trim(const string_type& s) { + auto it = s.begin(); + while (it != s.end() && isspace(*it)) { + it++; + } + auto rit = s.rbegin(); + while (rit.base() != it && isspace(*rit)) { + rit++; + } + return {it, rit.base()}; +} + +template <typename string_type> +string_type html_escape(const string_type& s) { + string_type ret; + ret.reserve(s.size()*2); + for (const auto ch : s) { + switch (ch) { + case '&': + ret.append({'&','a','m','p',';'}); + break; + case '<': + ret.append({'&','l','t',';'}); + break; + case '>': + ret.append({'&','g','t',';'}); + break; + case '\"': + ret.append({'&','q','u','o','t',';'}); + break; + case '\'': + ret.append({'&','a','p','o','s',';'}); + break; + default: + ret.append(1, ch); + break; + } + } + return ret; +} + +template <typename string_type> +std::vector<string_type> split(const string_type& s, typename string_type::value_type delim) { + std::vector<string_type> elems; + std::basic_stringstream<typename string_type::value_type> ss(s); + string_type item; + while (std::getline(ss, item, delim)) { + elems.push_back(item); + } + return elems; +} + +template <typename string_type> +class basic_renderer { +public: + using type1 = std::function<string_type(const string_type&)>; + using type2 = std::function<string_type(const string_type&, bool escaped)>; + + string_type operator()(const string_type& text) const { + return type1_(text); + } + + string_type operator()(const string_type& text, bool escaped) const { + return type2_(text, escaped); + } + +private: + basic_renderer(const type1& t1, const type2& t2) + : type1_(t1) + , type2_(t2) + {} + + const type1& type1_; + const type2& type2_; + + template <typename StringType> + friend class basic_mustache; +}; + +template <typename string_type> +class basic_lambda_t { +public: + using type1 = std::function<string_type(const string_type&)>; + using type2 = std::function<string_type(const string_type&, const basic_renderer<string_type>& render)>; + + basic_lambda_t(const type1& t) : type1_(new type1(t)) {} + basic_lambda_t(const type2& t) : type2_(new type2(t)) {} + + bool is_type1() const { return static_cast<bool>(type1_); } + bool is_type2() const { return static_cast<bool>(type2_); } + + const type1& type1_value() const { return *type1_; } + const type2& type2_value() const { return *type2_; } + + // Copying + basic_lambda_t(const basic_lambda_t& l) { + if (l.type1_) { + type1_.reset(new type1(*l.type1_)); + } else if (l.type2_) { + type2_.reset(new type2(*l.type2_)); + } + } + + string_type operator()(const string_type& text) const { + return (*type1_)(text); + } + + string_type operator()(const string_type& text, const basic_renderer<string_type>& render) const { + return (*type2_)(text, render); + } + +private: + std::unique_ptr<type1> type1_; + std::unique_ptr<type2> type2_; +}; + +template <typename string_type> +class basic_data; +template <typename string_type> +using basic_object = std::unordered_map<string_type, basic_data<string_type>>; +template <typename string_type> +using basic_list = std::vector<basic_data<string_type>>; +template <typename string_type> +using basic_partial = std::function<string_type()>; +template <typename string_type> +using basic_lambda = typename basic_lambda_t<string_type>::type1; +template <typename string_type> +using basic_lambda2 = typename basic_lambda_t<string_type>::type2; + +template <typename string_type> +class basic_data { +public: + enum class type { + object, + string, + list, + bool_true, + bool_false, + partial, + lambda, + lambda2, + invalid, + }; + + // Construction + basic_data() : basic_data(type::object) { + } + basic_data(const string_type& string) : type_{type::string} { + str_.reset(new string_type(string)); + } + basic_data(const typename string_type::value_type* string) : type_{type::string} { + str_.reset(new string_type(string)); + } + basic_data(const basic_object<string_type>& obj) : type_{type::object} { + obj_.reset(new basic_object<string_type>(obj)); + } + basic_data(const basic_list<string_type>& l) : type_{type::list} { + list_.reset(new basic_list<string_type>(l)); + } + basic_data(type t) : type_{t} { + switch (type_) { + case type::object: + obj_.reset(new basic_object<string_type>); + break; + case type::string: + str_.reset(new string_type); + break; + case type::list: + list_.reset(new basic_list<string_type>); + break; + default: + break; + } + } + basic_data(const string_type& name, const basic_data& var) : basic_data{} { + set(name, var); + } + basic_data(const basic_partial<string_type>& p) : type_{type::partial} { + partial_.reset(new basic_partial<string_type>(p)); + } + basic_data(const basic_lambda<string_type>& l) : type_{type::lambda} { + lambda_.reset(new basic_lambda_t<string_type>(l)); + } + basic_data(const basic_lambda2<string_type>& l) : type_{type::lambda2} { + lambda_.reset(new basic_lambda_t<string_type>(l)); + } + basic_data(const basic_lambda_t<string_type>& l) { + if (l.is_type1()) { + type_ = type::lambda; + } else if (l.is_type2()) { + type_ = type::lambda2; + } + lambda_.reset(new basic_lambda_t<string_type>(l)); + } + basic_data(bool b) : type_{b ? type::bool_true : type::bool_false} { + } + + // Copying + basic_data(const basic_data& dat) : type_(dat.type_) { + if (dat.obj_) { + obj_.reset(new basic_object<string_type>(*dat.obj_)); + } else if (dat.str_) { + str_.reset(new string_type(*dat.str_)); + } else if (dat.list_) { + list_.reset(new basic_list<string_type>(*dat.list_)); + } else if (dat.partial_) { + partial_.reset(new basic_partial<string_type>(*dat.partial_)); + } else if (dat.lambda_) { + lambda_.reset(new basic_lambda_t<string_type>(*dat.lambda_)); + } + } + + // Move + basic_data(basic_data&& dat) : type_{dat.type_} { + if (dat.obj_) { + obj_ = std::move(dat.obj_); + } else if (dat.str_) { + str_ = std::move(dat.str_); + } else if (dat.list_) { + list_ = std::move(dat.list_); + } else if (dat.partial_) { + partial_ = std::move(dat.partial_); + } else if (dat.lambda_) { + lambda_ = std::move(dat.lambda_); + } + dat.type_ = type::invalid; + } + basic_data& operator= (basic_data&& dat) { + if (this != &dat) { + obj_.reset(); + str_.reset(); + list_.reset(); + partial_.reset(); + lambda_.reset(); + if (dat.obj_) { + obj_ = std::move(dat.obj_); + } else if (dat.str_) { + str_ = std::move(dat.str_); + } else if (dat.list_) { + list_ = std::move(dat.list_); + } else if (dat.partial_) { + partial_ = std::move(dat.partial_); + } else if (dat.lambda_) { + lambda_ = std::move(dat.lambda_); + } + type_ = dat.type_; + dat.type_ = type::invalid; + } + return *this; + } + + // Type info + bool is_object() const { + return type_ == type::object; + } + bool is_string() const { + return type_ == type::string; + } + bool is_list() const { + return type_ == type::list; + } + bool is_bool() const { + return is_true() || is_false(); + } + bool is_true() const { + return type_ == type::bool_true; + } + bool is_false() const { + return type_ == type::bool_false; + } + bool is_partial() const { + return type_ == type::partial; + } + bool is_lambda() const { + return type_ == type::lambda; + } + bool is_lambda2() const { + return type_ == type::lambda2; + } + bool is_invalid() const { + return type_ == type::invalid; + } + + // Object data + bool is_empty_object() const { + return is_object() && obj_->empty(); + } + bool is_non_empty_object() const { + return is_object() && !obj_->empty(); + } + void set(const string_type& name, const basic_data& var) { + if (is_object()) { + auto it = obj_->find(name); + if (it != obj_->end()) { + obj_->erase(it); + } + obj_->insert(std::pair<string_type,basic_data>{name, var}); + } + } + const basic_data* get(const string_type& name) const { + if (!is_object()) { + return nullptr; + } + const auto& it = obj_->find(name); + if (it == obj_->end()) { + return nullptr; + } + return &it->second; + } + + // List data + void push_back(const basic_data& var) { + if (is_list()) { + list_->push_back(var); + } + } + const basic_list<string_type>& list_value() const { + return *list_; + } + bool is_empty_list() const { + return is_list() && list_->empty(); + } + bool is_non_empty_list() const { + return is_list() && !list_->empty(); + } + basic_data& operator<< (const basic_data& data) { + push_back(data); + return *this; + } + + // String data + const string_type& string_value() const { + return *str_; + } + + basic_data& operator[] (const string_type& key) { + return (*obj_)[key]; + } + + const basic_partial<string_type>& partial_value() const { + return (*partial_); + } + + const basic_lambda<string_type>& lambda_value() const { + return lambda_->type1_value(); + } + + const basic_lambda2<string_type>& lambda2_value() const { + return lambda_->type2_value(); + } + +private: + type type_; + std::unique_ptr<basic_object<string_type>> obj_; + std::unique_ptr<string_type> str_; + std::unique_ptr<basic_list<string_type>> list_; + std::unique_ptr<basic_partial<string_type>> partial_; + std::unique_ptr<basic_lambda_t<string_type>> lambda_; +}; + +template <typename string_type> +class delimiter_set { +public: + string_type begin; + string_type end; + delimiter_set() + : begin(default_begin) + , end(default_end) + {} + bool is_default() const { return begin == default_begin && end == default_end; } + static const string_type default_begin; + static const string_type default_end; +}; + +template <typename string_type> +const string_type delimiter_set<string_type>::default_begin(2, '{'); +template <typename string_type> +const string_type delimiter_set<string_type>::default_end(2, '}'); + +template <typename string_type> +class basic_context { +public: + virtual void push(const basic_data<string_type>* data) = 0; + virtual void pop() = 0; + + virtual const basic_data<string_type>* get(const string_type& name) const = 0; + virtual const basic_data<string_type>* get_partial(const string_type& name) const = 0; +}; + +template <typename string_type> +class context : public basic_context<string_type> { +public: + context(const basic_data<string_type>* data) { + push(data); + } + + context() { + } + + virtual void push(const basic_data<string_type>* data) override { + items_.insert(items_.begin(), data); + } + + virtual void pop() override { + items_.erase(items_.begin()); + } + + virtual const basic_data<string_type>* get(const string_type& name) const override { + // process {{.}} name + if (name.size() == 1 && name.at(0) == '.') { + return items_.front(); + } + if (name.find('.') == string_type::npos) { + // process normal name without having to split which is slower + for (const auto& item : items_) { + const auto var = item->get(name); + if (var) { + return var; + } + } + return nullptr; + } + // process x.y-like name + const auto names = split(name, '.'); + for (const auto& item : items_) { + auto var = item; + for (const auto& n : names) { + var = var->get(n); + if (!var) { + break; + } + } + if (var) { + return var; + } + } + return nullptr; + } + + virtual const basic_data<string_type>* get_partial(const string_type& name) const override { + for (const auto& item : items_) { + const auto var = item->get(name); + if (var) { + return var; + } + } + return nullptr; + } + + context(const context&) = delete; + context& operator= (const context&) = delete; + +private: + std::vector<const basic_data<string_type>*> items_; +}; + +template <typename string_type> +class context_internal { +public: + basic_context<string_type>& ctx; + delimiter_set<string_type> delim_set; + + context_internal(basic_context<string_type>& a_ctx) + : ctx(a_ctx) + { + } +}; + +enum class tag_type { + text, + variable, + unescaped_variable, + section_begin, + section_end, + section_begin_inverted, + comment, + partial, + set_delimiter, +}; + +template <typename string_type> +class mstch_tag /* gcc doesn't allow "tag tag;" so rename the class :( */ { +public: + string_type name; + tag_type type = tag_type::text; + std::shared_ptr<string_type> section_text; + std::shared_ptr<delimiter_set<string_type>> delim_set; + bool is_section_begin() const { + return type == tag_type::section_begin || type == tag_type::section_begin_inverted; + } + bool is_section_end() const { + return type == tag_type::section_end; + } +}; + +template <typename string_type> +class context_pusher { +public: + context_pusher(context_internal<string_type>& ctx, const basic_data<string_type>* data) + : ctx_(ctx) + { + ctx.ctx.push(data); + } + ~context_pusher() { + ctx_.ctx.pop(); + } + context_pusher(const context_pusher&) = delete; + context_pusher& operator= (const context_pusher&) = delete; +private: + context_internal<string_type>& ctx_; +}; + +template <typename string_type> +class component { +private: + using string_size_type = typename string_type::size_type; + +public: + string_type text; + mstch_tag<string_type> tag; + std::vector<component> children; + string_size_type position = string_type::npos; + + enum class walk_control { + walk, // "continue" is reserved :/ + stop, + skip, + }; + using walk_callback = std::function<walk_control(component&)>; + + component() {} + component(const string_type& t, string_size_type p) : text(t), position(p) {} + + bool is_text() const { + return tag.type == tag_type::text; + } + + bool is_newline() const { + return is_text() && ((text.size() == 2 && text[0] == '\r' && text[1] == '\n') || + (text.size() == 1 && (text[0] == '\n' || text[0] == '\r'))); + } + + bool is_non_newline_whitespace() const { + return is_text() && !is_newline() && text.size() == 1 && (text[0] == ' ' || text[0] == '\t'); + } + + void walk_children(const walk_callback& callback) { + for (auto& child : children) { + if (child.walk(callback) != walk_control::walk) { + break; + } + } + } + +private: + walk_control walk(const walk_callback& callback) { + walk_control control{callback(*this)}; + if (control == walk_control::stop) { + return control; + } else if (control == walk_control::skip) { + return walk_control::walk; + } + for (auto& child : children) { + control = child.walk(callback); + assert(control == walk_control::walk); + } + return control; + } +}; + +template <typename string_type> +class parser { +public: + parser(const string_type& input, context_internal<string_type>& ctx, component<string_type>& root_component, string_type& error_message) + { + parse(input, ctx, root_component, error_message); + } + +private: + void parse(const string_type& input, context_internal<string_type>& ctx, component<string_type>& root_component, string_type& error_message) const { + using string_size_type = typename string_type::size_type; + using streamstring = std::basic_ostringstream<typename string_type::value_type>; + + const string_type brace_delimiter_end_unescaped(3, '}'); + const string_size_type input_size{input.size()}; + + bool current_delimiter_is_brace{ctx.delim_set.is_default()}; + + std::vector<component<string_type>*> sections{&root_component}; + std::vector<string_size_type> section_starts; + string_type current_text; + string_size_type current_text_position = -1; + + current_text.reserve(input_size); + + const auto process_current_text = [¤t_text, ¤t_text_position, §ions]() { + if (!current_text.empty()) { + const component<string_type> comp{current_text, current_text_position}; + sections.back()->children.push_back(comp); + current_text.clear(); + current_text_position = -1; + } + }; + + const std::vector<string_type> whitespace{ + string_type(1, '\r') + string_type(1, '\n'), + string_type(1, '\n'), + string_type(1, '\r'), + string_type(1, ' '), + string_type(1, '\t'), + }; + + for (string_size_type input_position = 0; input_position != input_size;) { + bool parse_tag = false; + + if (input.compare(input_position, ctx.delim_set.begin.size(), ctx.delim_set.begin) == 0) { + process_current_text(); + + // Tag start delimiter + parse_tag = true; + } else { + bool parsed_whitespace = false; + for (const auto& whitespace_text : whitespace) { + if (input.compare(input_position, whitespace_text.size(), whitespace_text) == 0) { + process_current_text(); + + const component<string_type> comp{whitespace_text, input_position}; + sections.back()->children.push_back(comp); + input_position += whitespace_text.size(); + + parsed_whitespace = true; + break; + } + } + + if (!parsed_whitespace) { + if (current_text.empty()) { + current_text_position = input_position; + } + current_text.append(1, input[input_position]); + input_position++; + } + } + + if (!parse_tag) { + continue; + } + + // Find the next tag start delimiter + const string_size_type tag_location_start = input_position; + + // Find the next tag end delimiter + string_size_type tag_contents_location{tag_location_start + ctx.delim_set.begin.size()}; + const bool tag_is_unescaped_var{current_delimiter_is_brace && tag_location_start != (input_size - 2) && input.at(tag_contents_location) == ctx.delim_set.begin.at(0)}; + const string_type& current_tag_delimiter_end{tag_is_unescaped_var ? brace_delimiter_end_unescaped : ctx.delim_set.end}; + const auto current_tag_delimiter_end_size = current_tag_delimiter_end.size(); + if (tag_is_unescaped_var) { + ++tag_contents_location; + } + const string_size_type tag_location_end{input.find(current_tag_delimiter_end, tag_contents_location)}; + if (tag_location_end == string_type::npos) { + streamstring ss; + ss << "Unclosed tag at " << tag_location_start; + error_message.assign(ss.str()); + return; + } + + // Parse tag + const string_type tag_contents{trim(string_type{input, tag_contents_location, tag_location_end - tag_contents_location})}; + component<string_type> comp; + if (!tag_contents.empty() && tag_contents[0] == '=') { + if (!parse_set_delimiter_tag(tag_contents, ctx.delim_set)) { + streamstring ss; + ss << "Invalid set delimiter tag at " << tag_location_start; + error_message.assign(ss.str()); + return; + } + current_delimiter_is_brace = ctx.delim_set.is_default(); + comp.tag.type = tag_type::set_delimiter; + comp.tag.delim_set.reset(new delimiter_set<string_type>(ctx.delim_set)); + } + if (comp.tag.type != tag_type::set_delimiter) { + parse_tag_contents(tag_is_unescaped_var, tag_contents, comp.tag); + } + comp.position = tag_location_start; + sections.back()->children.push_back(comp); + + // Start next search after this tag + input_position = tag_location_end + current_tag_delimiter_end_size; + + // Push or pop sections + if (comp.tag.is_section_begin()) { + sections.push_back(§ions.back()->children.back()); + section_starts.push_back(input_position); + } else if (comp.tag.is_section_end()) { + if (sections.size() == 1) { + streamstring ss; + ss << "Unopened section \"" << comp.tag.name << "\" at " << comp.position; + error_message.assign(ss.str()); + return; + } + sections.back()->tag.section_text.reset(new string_type(input.substr(section_starts.back(), tag_location_start - section_starts.back()))); + sections.pop_back(); + section_starts.pop_back(); + } + } + + process_current_text(); + + // Check for sections without an ending tag + root_component.walk_children([&error_message](component<string_type>& comp) -> typename component<string_type>::walk_control { + if (!comp.tag.is_section_begin()) { + return component<string_type>::walk_control::walk; + } + if (comp.children.empty() || !comp.children.back().tag.is_section_end() || comp.children.back().tag.name != comp.tag.name) { + streamstring ss; + ss << "Unclosed section \"" << comp.tag.name << "\" at " << comp.position; + error_message.assign(ss.str()); + return component<string_type>::walk_control::stop; + } + comp.children.pop_back(); // remove now useless end section component + return component<string_type>::walk_control::walk; + }); + if (!error_message.empty()) { + return; + } + } + + bool is_set_delimiter_valid(const string_type& delimiter) const { + // "Custom delimiters may not contain whitespace or the equals sign." + for (const auto ch : delimiter) { + if (ch == '=' || isspace(ch)) { + return false; + } + } + return true; + } + + bool parse_set_delimiter_tag(const string_type& contents, delimiter_set<string_type>& delimiter_set) const { + // Smallest legal tag is "=X X=" + if (contents.size() < 5) { + return false; + } + if (contents.back() != '=') { + return false; + } + const auto contents_substr = trim(contents.substr(1, contents.size() - 2)); + const auto spacepos = contents_substr.find(' '); + if (spacepos == string_type::npos) { + return false; + } + const auto nonspace = contents_substr.find_first_not_of(' ', spacepos + 1); + assert(nonspace != string_type::npos); + const string_type begin = contents_substr.substr(0, spacepos); + const string_type end = contents_substr.substr(nonspace, contents_substr.size() - nonspace); + if (!is_set_delimiter_valid(begin) || !is_set_delimiter_valid(end)) { + return false; + } + delimiter_set.begin = begin; + delimiter_set.end = end; + return true; + } + + void parse_tag_contents(bool is_unescaped_var, const string_type& contents, mstch_tag<string_type>& tag) const { + if (is_unescaped_var) { + tag.type = tag_type::unescaped_variable; + tag.name = contents; + } else if (contents.empty()) { + tag.type = tag_type::variable; + tag.name.clear(); + } else { + switch (contents.at(0)) { + case '#': + tag.type = tag_type::section_begin; + break; + case '^': + tag.type = tag_type::section_begin_inverted; + break; + case '/': + tag.type = tag_type::section_end; + break; + case '>': + tag.type = tag_type::partial; + break; + case '&': + tag.type = tag_type::unescaped_variable; + break; + case '!': + tag.type = tag_type::comment; + break; + default: + tag.type = tag_type::variable; + break; + } + if (tag.type == tag_type::variable) { + tag.name = contents; + } else { + string_type name{contents}; + name.erase(name.begin()); + tag.name = trim(name); + } + } + } +}; + +template <typename StringType> +class basic_mustache { +public: + using string_type = StringType; + + basic_mustache(const string_type& input) + : basic_mustache() { + context<string_type> ctx; + context_internal<string_type> context{ctx}; + parser<string_type> parser{input, context, root_component_, error_message_}; + } + + bool is_valid() const { + return error_message_.empty(); + } + + const string_type& error_message() const { + return error_message_; + } + + using escape_handler = std::function<string_type(const string_type&)>; + void set_custom_escape(const escape_handler& escape_fn) { + escape_ = escape_fn; + } + + template <typename stream_type> + stream_type& render(const basic_data<string_type>& data, stream_type& stream) { + render(data, [&stream](const string_type& str) { + stream << str; + }); + return stream; + } + + string_type render(const basic_data<string_type>& data) { + std::basic_ostringstream<typename string_type::value_type> ss; + return render(data, ss).str(); + } + + template <typename stream_type> + stream_type& render(basic_context<string_type>& ctx, stream_type& stream) { + context_internal<string_type> context{ctx}; + render([&stream](const string_type& str) { + stream << str; + }, context); + return stream; + } + + string_type render(basic_context<string_type>& ctx) { + std::basic_ostringstream<typename string_type::value_type> ss; + return render(ctx, ss).str(); + } + + using render_handler = std::function<void(const string_type&)>; + void render(const basic_data<string_type>& data, const render_handler& handler) { + if (!is_valid()) { + return; + } + context<string_type> ctx{&data}; + context_internal<string_type> context{ctx}; + render(handler, context); + } + +private: + using string_size_type = typename string_type::size_type; + + basic_mustache() + : escape_(html_escape<string_type>) + { + } + + basic_mustache(const string_type& input, context_internal<string_type>& ctx) + : basic_mustache() { + parser<string_type> parser{input, ctx, root_component_, error_message_}; + } + + void walk(const typename component<string_type>::walk_callback& callback) { + root_component_.walk_children(callback); + } + + string_type render(context_internal<string_type>& ctx) { + std::basic_ostringstream<typename string_type::value_type> ss; + render([&ss](const string_type& str) { + ss << str; + }, ctx); + return ss.str(); + } + + void render(const render_handler& handler, context_internal<string_type>& ctx) { + walk([&handler, &ctx, this](component<string_type>& comp) -> typename component<string_type>::walk_control { + return render_component(handler, ctx, comp); + }); + } + + typename component<string_type>::walk_control render_component(const render_handler& handler, context_internal<string_type>& ctx, component<string_type>& comp) { + if (comp.is_text()) { + handler(comp.text); + return component<string_type>::walk_control::walk; + } + + const mstch_tag<string_type>& tag{comp.tag}; + const basic_data<string_type>* var = nullptr; + switch (tag.type) { + case tag_type::variable: + case tag_type::unescaped_variable: + if ((var = ctx.ctx.get(tag.name)) != nullptr) { + if (!render_variable(handler, var, ctx, tag.type == tag_type::variable)) { + return component<string_type>::walk_control::stop; + } + } + break; + case tag_type::section_begin: + if ((var = ctx.ctx.get(tag.name)) != nullptr) { + if (var->is_lambda() || var->is_lambda2()) { + if (!render_lambda(handler, var, ctx, render_lambda_escape::optional, *comp.tag.section_text, true)) { + return component<string_type>::walk_control::stop; + } + } else if (!var->is_false() && !var->is_empty_list()) { + render_section(handler, ctx, comp, var); + } + } + return component<string_type>::walk_control::skip; + case tag_type::section_begin_inverted: + if ((var = ctx.ctx.get(tag.name)) == nullptr || var->is_false() || var->is_empty_list()) { + render_section(handler, ctx, comp, var); + } + return component<string_type>::walk_control::skip; + case tag_type::partial: + if ((var = ctx.ctx.get_partial(tag.name)) != nullptr && (var->is_partial() || var->is_string())) { + const auto partial_result = var->is_partial() ? var->partial_value()() : var->string_value(); + basic_mustache tmpl{partial_result}; + tmpl.set_custom_escape(escape_); + if (!tmpl.is_valid()) { + error_message_ = tmpl.error_message(); + } else { + tmpl.render(handler, ctx); + if (!tmpl.is_valid()) { + error_message_ = tmpl.error_message(); + } + } + if (!tmpl.is_valid()) { + return component<string_type>::walk_control::stop; + } + } + break; + case tag_type::set_delimiter: + ctx.delim_set = *comp.tag.delim_set; + break; + default: + break; + } + + return component<string_type>::walk_control::walk; + } + + enum class render_lambda_escape { + escape, + unescape, + optional, + }; + + bool render_lambda(const render_handler& handler, const basic_data<string_type>* var, context_internal<string_type>& ctx, render_lambda_escape escape, const string_type& text, bool parse_with_same_context) { + const typename basic_renderer<string_type>::type2 render2 = [this, &ctx, parse_with_same_context, escape](const string_type& text, bool escaped) { + const auto process_template = [this, &ctx, escape, escaped](basic_mustache& tmpl) -> string_type { + if (!tmpl.is_valid()) { + error_message_ = tmpl.error_message(); + return {}; + } + const string_type str{tmpl.render(ctx)}; + if (!tmpl.is_valid()) { + error_message_ = tmpl.error_message(); + return {}; + } + bool do_escape = false; + switch (escape) { + case render_lambda_escape::escape: + do_escape = true; + break; + case render_lambda_escape::unescape: + do_escape = false; + break; + case render_lambda_escape::optional: + do_escape = escaped; + break; + } + return do_escape ? escape_(str) : str; + }; + if (parse_with_same_context) { + basic_mustache tmpl{text, ctx}; + tmpl.set_custom_escape(escape_); + return process_template(tmpl); + } + basic_mustache tmpl{text}; + tmpl.set_custom_escape(escape_); + return process_template(tmpl); + }; + const typename basic_renderer<string_type>::type1 render = [&render2](const string_type& text) { + return render2(text, false); + }; + if (var->is_lambda2()) { + const basic_renderer<string_type> renderer{render, render2}; + handler(var->lambda2_value()(text, renderer)); + } else { + handler(render(var->lambda_value()(text))); + } + return error_message_.empty(); + } + + bool render_variable(const render_handler& handler, const basic_data<string_type>* var, context_internal<string_type>& ctx, bool escaped) { + if (var->is_string()) { + const auto varstr = var->string_value(); + handler(escaped ? escape_(varstr) : varstr); + } else if (var->is_lambda()) { + const render_lambda_escape escape_opt = escaped ? render_lambda_escape::escape : render_lambda_escape::unescape; + return render_lambda(handler, var, ctx, escape_opt, {}, false); + } else if (var->is_lambda2()) { + using streamstring = std::basic_ostringstream<typename string_type::value_type>; + streamstring ss; + ss << "Lambda with render argument is not allowed for regular variables"; + error_message_ = ss.str(); + return false; + } + return true; + } + + void render_section(const render_handler& handler, context_internal<string_type>& ctx, component<string_type>& incomp, const basic_data<string_type>* var) { + const auto callback = [&handler, &ctx, this](component<string_type>& comp) -> typename component<string_type>::walk_control { + return render_component(handler, ctx, comp); + }; + if (var && var->is_non_empty_list()) { + for (const auto& item : var->list_value()) { + const context_pusher<string_type> ctxpusher{ctx, &item}; + incomp.walk_children(callback); + } + } else if (var) { + const context_pusher<string_type> ctxpusher{ctx, var}; + incomp.walk_children(callback); + } else { + incomp.walk_children(callback); + } + } + +private: + string_type error_message_; + component<string_type> root_component_; + escape_handler escape_; +}; + +using mustache = basic_mustache<std::string>; +using data = basic_data<mustache::string_type>; +using object = basic_object<mustache::string_type>; +using list = basic_list<mustache::string_type>; +using partial = basic_partial<mustache::string_type>; +using renderer = basic_renderer<mustache::string_type>; +using lambda = basic_lambda<mustache::string_type>; +using lambda2 = basic_lambda2<mustache::string_type>; +using lambda_t = basic_lambda_t<mustache::string_type>; + +using mustachew = basic_mustache<std::wstring>; +using dataw = basic_data<mustachew::string_type>; + +} // namespace mustache +} // namespace kainjow + +#endif // KAINJOW_MUSTACHE_HPP
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/libmustache/CMakeLists.txt Thu Jul 25 20:30:00 2019 +0000 @@ -0,0 +1,23 @@ +# +# CMakeLists.txt -- test project for mustache +# +# Copyright (c) 2016-2018 David Demelier <markand@malikania.fr> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +cmake_minimum_required(VERSION 3.7) +project(test-mustache) +add_subdirectory(../../libmustache mustache-build-dir) +add_executable(test-mustache main.cpp) +target_link_libraries(test-mustache libmustache)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/libmustache/main.cpp Thu Jul 25 20:30:00 2019 +0000 @@ -0,0 +1,28 @@ +/* + * main.cpp -- main file + * + * Copyright (c) 2016-2018 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <mustache.hpp> + +using kainjow::mustache::mustache; + +int main() +{ + mustache tmpl("Hello {{who}}"); + + return tmpl.render({"who", "markand"}) == "Hello markand" ? 0 : 1; +}