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 = [&current_text, &current_text_position, &sections]() {
+            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(&sections.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;
+}