/*
 * Boost Software License - Version 1.0
 *
 * Copyright 2015-2020 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.
 */

#include "mustache.hpp"

#define CATCH_CONFIG_MAIN
#include "catch.hpp"

using namespace kainjow::mustache;

#ifndef MUSTACHE_VS2013
    #if defined(_MSC_VER) && _MSC_VER == 1800
        #define MUSTACHE_VS2013 1
    #else
        #define MUSTACHE_VS2013 0
    #endif
#endif

TEST_CASE("split") {

    std::vector<std::string> names;
    names = split<std::string>("", '.');
    REQUIRE(names.size() == 0);
    names = split<std::string>("test", '.');
    REQUIRE(names.size() == 1);
    CHECK(names[0] == "test");
    names = split<std::string>("a.b", '.');
    REQUIRE(names.size() == 2);
    CHECK(names[0] == "a");
    CHECK(names[1] == "b");
    names = split<std::string>(".", '.');
    REQUIRE(names.size() == 1);
    CHECK(names[0] == "");
    names = split<std::string>("a.", '.');
    REQUIRE(names.size() == 1);
    CHECK(names[0] == "a");

}

TEST_CASE("variables") {

    SECTION("empty") {
        mustache tmpl("");
        data data;
        CHECK(tmpl.render(data).empty());
    }

    SECTION("none") {
        mustache tmpl("Hello");
        data data;
        CHECK(tmpl.render(data) == "Hello");
    }

    SECTION("single_miss") {
        mustache tmpl("Hello {{name}}");
        data data;
        CHECK(tmpl.render(data) == "Hello ");
    }

    SECTION("single_exist") {
        mustache tmpl("Hello {{name}}");
        data data;
        data.set("name", "Steve");
        CHECK(tmpl.render(data) == "Hello Steve");
    }

    SECTION("single_exist_wide") {
        mustachew tmpl(L"Hello {{name}}");
        dataw data;
        data.set(L"name", L"Steve");
        CHECK(tmpl.render(data) == L"Hello Steve");
    }

    SECTION("escape") {
        mustache tmpl("Hello {{name}}");
        data data;
        data.set("name", "\"S\"<br>te&v\'e");
        CHECK(tmpl.render(data) == "Hello &quot;S&quot;&lt;br&gt;te&amp;v&apos;e");
    }

    SECTION("unescaped1") {
        mustache tmpl("Hello {{{name}}}");
        data data;
        data.set("name", "\"S\"<br>te&v\'e");
        CHECK(tmpl.render(data) == "Hello \"S\"<br>te&v\'e");
    }

    SECTION("unescaped2") {
        mustache tmpl("Hello {{&name}}");
        data data;
        data.set("name", "\"S\"<br>te&v\'e");
        CHECK(tmpl.render(data) == "Hello \"S\"<br>te&v\'e");
    }

    SECTION("unescaped2_spaces") {
        mustache tmpl("Hello {{   &      name  }}");
        data data;
        data.set("name", "\"S\"<br>te&v\'e");
        CHECK(tmpl.render(data) == "Hello \"S\"<br>te&v\'e");
    }

    SECTION("empty_name") {
        mustache tmpl("Hello {{}}");
        data data;
        data.set("", "Steve");
        CHECK(tmpl.render(data) == "Hello Steve");
    }

    SECTION("braces") {
        mustache tmpl("my {{var}}");
        data data;
        data.set("var", "{{te}}st");
        CHECK(tmpl.render(data) == "my {{te}}st");
    }

}

TEST_CASE("comments") {

    SECTION("simple") {
        mustache tmpl("<h1>Today{{! ignore me }}.</h1>");
        data data;
        CHECK(tmpl.render(data) == "<h1>Today.</h1>");
    }

    SECTION("newlines") {
        mustache tmpl("Hello\n{{! ignore me }}\nWorld\n");
        data data;
        CHECK(tmpl.render(data) == "Hello\n\nWorld\n");
    }

}

TEST_CASE("set_delimiter") {

    SECTION("basic") {
        mustache tmpl("{{name}}{{=<% %>=}}<% name %><%={{ }}=%>{{ name }}");
        data data;
        data.set("name", "Steve");
        CHECK(tmpl.render(data) == "SteveSteveSteve");
    }

    SECTION("small") {
        mustache tmpl("{{n}}{{=a b=}}anba={{ }}=b{{n}}");
        data data;
        data.set("n", "s");
        CHECK(tmpl.render(data) == "sss");
    }

    SECTION("noreset") {
        mustache tmpl("{{=[ ]=}}[name] [x] + [y] = [sum]");
        data data;
        data.set("name", "Steve");
        data.set("x", "1");
        data.set("y", "2");
        data.set("sum", "3");
        CHECK(tmpl.render(data) == "Steve 1 + 2 = 3");
    }

    SECTION("whitespace") {
        mustache tmpl("|{{= @   @ =}}|");
        data data;
        REQUIRE(tmpl.is_valid());
        CHECK(tmpl.render(data) == "||");
    }

}

TEST_CASE("sections") {

    SECTION("nonexistant") {
        mustache tmpl("{{#var}}not shown{{/var}}");
        data data;
        CHECK(tmpl.render(data) == "");
    }

    SECTION("false") {
        mustache tmpl("{{#var}}not shown{{/var}}");
        data dat;
        dat.set("var", data(data::type::bool_false));
        CHECK(tmpl.render(dat) == "");
    }

    SECTION("emptylist") {
        mustache tmpl("{{#var}}not shown{{/var}}");
        data dat;
        dat.set("var", data(data::type::list));
        CHECK(tmpl.render(dat) == "");
    }

    SECTION("nested") {
        mustache tmpl("{{#var1}}hello{{#var2}}world{{/var2}}{{/var1}}");
        data data;
        data.set("var1", data::type::bool_true);
        data.set("var2", data::type::bool_true);
        CHECK(tmpl.render(data) == "helloworld");
    }

}

TEST_CASE("sections_inverted") {

    SECTION("nonexistant") {
        mustache tmpl("{{^var}}shown{{/var}}");
        CHECK(tmpl.render(data()) == "shown");
    }

    SECTION("false") {
        mustache tmpl("{{^var}}shown{{/var}}");
        data dat("var", data(data::type::bool_false));
        CHECK(tmpl.render(dat) == "shown");
    }

    SECTION("emptylist") {
        mustache tmpl("{{^var}}shown{{/var}}");
        data dat("var", data(data::type::list));
        CHECK(tmpl.render(dat) == "shown");
    }

}

TEST_CASE("section_lists") {

    SECTION("list") {
        mustache tmpl("{{#people}}Hello {{name}}, {{/people}}");
        data people = data::type::list;
        for (const auto& name : {"Steve", "Bill", "Tim"}) {
            people.push_back(data("name", name));
        }
        data data("people", people);
        CHECK(tmpl.render(data) == "Hello Steve, Hello Bill, Hello Tim, ");
    }

    SECTION("nested") {
        mustache tmpl("{{#families}}surname={{surname}}, members={{#members}}{{given}},{{/members}}|{{/families}}");
        data families = data::type::list;
        data family1;
        family1.set("surname", "Smith");
        data members1 = data::type::list;
        data member1a; member1a.set("given", "Steve"); members1.push_back(member1a);
        data member1b; member1b.set("given", "Joe"); members1.push_back(member1b);
        family1.set("members", members1);
        data family2;
        family2.set("surname", "Lee");
        data members2 = data::type::list;
        data member2a; member2a.set("given", "Bill"); members2.push_back(member2a);
        data member2b; member2b.set("given", "Peter"); members2.push_back(member2b);
        family2.set("members", members2);
        families.push_back(family1);
        families.push_back(family2);
        data data;
        data.set("families", families);
        CHECK(tmpl.render(data) == "surname=Smith, members=Steve,Joe,|surname=Lee, members=Bill,Peter,|");
    }

    SECTION("dot") {
        mustache tmpl("{{#names}}Hello {{.}}, {{/names}}");
        data names = data::type::list;
        names.push_back(data("Steve"));
        names.push_back(data("Bill"));
        names.push_back(data("Tim"));
        data data("names", names);
        CHECK(tmpl.render(data) == "Hello Steve, Hello Bill, Hello Tim, ");
    }

    SECTION("dot2") {
        mustache tmpl("{{#names}}Hello {{.}}{{/names}}{{#friends}} and {{.}}{{/friends}}");
        data friends = data::type::list;
        friends.push_back("Bill");
        friends.push_back("Tim");
        data data;
        data.set("friends", friends);
        CHECK(tmpl.render(data) == " and Bill and Tim");
        data.set("names", "Steve");
        CHECK(tmpl.render(data) == "Hello Steve and Bill and Tim");
    }

}

TEST_CASE("section_object") {

    SECTION("basic") {
        mustache tmpl("{{#employee}}name={{name}}, age={{age}}{{/employee}}");
        data person;
        person.set("name", "Steve");
        person.set("age", "42");
        data data;
        data.set("employee", person);
        CHECK(tmpl.render(data) == "name=Steve, age=42");
    }

    SECTION("basic_parent") {
        mustache tmpl("({{subject}}) {{#employee}}name={{name}}, age={{age}} - {{subject}}{{/employee}}");
        data person;
        person.set("name", "Steve");
        person.set("age", "42");
        person.set("subject", "email");
        data data;
        data.set("subject", "test");
        data.set("employee", person);
        CHECK(tmpl.render(data) == "(test) name=Steve, age=42 - email");
    }

}

TEST_CASE("examples") {

    SECTION("one") {
        mustache tmpl{"Hello {{what}}!"};
        std::cout << tmpl.render({"what", "World"}) << std::endl;
        CHECK(tmpl.is_valid());
        CHECK(tmpl.error_message() == "");
        CHECK(tmpl.render({"what", "World"}) == "Hello World!");
    }

    SECTION("two") {
        mustache tmpl{"{{#employees}}{{name}}, {{/employees}}"};
        data employees{data::type::list};
        employees << data{"name", "Steve"} << data{"name", "Bill"};
        std::ostream& stream = tmpl.render({"employees", employees}, std::cout) << std::endl;
        CHECK(tmpl.is_valid());
        CHECK(tmpl.error_message() == "");
        CHECK(tmpl.render({"employees", employees}) == "Steve, Bill, ");
        CHECK(&stream == &std::cout);
    }

    SECTION("three") {
        mustache tmpl("Hello {{what}}!");
        std::stringstream ss;
        tmpl.render({"what", "World"}, [&ss](const std::string& str) {
            ss << str;
        });
        CHECK(tmpl.is_valid());
        CHECK(tmpl.error_message() == "");
        CHECK(ss.str() == "Hello World!");
    }

}

TEST_CASE("data") {

    SECTION("types") {
        data dat("age", "42");
        data emptyStr = data::type::string;
        dat["name"] = "Steve";
        dat["is_human"] = data::type::bool_true;
        dat["is_dog"] = false;
        dat["is_organic"] = true;
        const data* name;
        const data* age;
        const data* is_human;
        name = dat.get("name");
        age = dat.get("age");
        is_human = dat.get("is_human");
        CHECK(name != (const data*)0);
        CHECK(age != (const data*)0);
        CHECK(is_human != (const data*)0);
        CHECK(dat.get("miss") == (const data*)0);
        REQUIRE(name->is_string());
        CHECK(name->string_value() == "Steve");
        REQUIRE(age->is_string());
        CHECK(age->string_value() == "42");
        CHECK(is_human->is_true());
        CHECK(is_human->is_bool());
        CHECK(emptyStr.is_string());
        CHECK(emptyStr.string_value() == "");
        CHECK(dat["is_dog"].is_bool());
        CHECK(dat["is_dog"].is_false());
        CHECK(dat["is_organic"].is_bool());
        CHECK(dat["is_organic"].is_true());

        data emptyData;
        CHECK(emptyData.is_empty_object() == true);
        CHECK(emptyData.is_non_empty_object() == false);

        data nonEmptyData("name", "foo");
        CHECK(nonEmptyData.is_empty_object() == false);
        CHECK(nonEmptyData.is_non_empty_object() == true);
    }

    SECTION("move_ctor") {
        data obj1{data::type::list};
        CHECK(obj1.is_list());
        data obj2{std::move(obj1)};
        CHECK(obj2.is_list());
        CHECK(obj1.is_invalid());
        obj2.push_back({"name", "Steve"}); // this should puke if the internal data isn't setup correctly
    }

    SECTION("move_assign") {
        data obj1{data::type::list};
        CHECK(obj1.is_list());
        data obj2 = std::move(obj1);
        CHECK(obj2.is_list());
        CHECK(obj1.is_invalid());
        obj2.push_back({"name", "Steve"}); // this should puke if the internal data isn't setup correctly

        data lambda0{lambda{[](const std::string&){ return ""; }}};
        CHECK(lambda0.is_lambda());
        data lambda1 = std::move(lambda0);
        CHECK(lambda1.is_lambda());
        CHECK(lambda0.is_invalid());

        data lambda3{lambda2{[](const std::string&, const renderer&){ return ""; }}};
        CHECK(lambda3.is_lambda2());
        data lambda4 = std::move(lambda3);
        CHECK(lambda4.is_lambda2());
        CHECK(lambda3.is_invalid());
    }

    SECTION("lambda_copy_ctor") {
        data l1{lambda{[](const std::string&){ return ""; }}};
        data l2{l1};
        CHECK(l1.is_lambda());
        CHECK(l2.is_lambda());
        CHECK(l1.lambda_value()("") == l2.lambda_value()(""));
    }

    SECTION("lambda2_copy_ctor") {
        data l1{lambda2{[](const std::string&, const renderer& r){ return r(""); }}};
        data l2{l1};
        CHECK(l1.is_lambda2());
        CHECK(l2.is_lambda2());
    }

    SECTION("data_set") {
        data data;
        data.set("var", data::type::bool_true);
        CHECK(data.get("var")->is_bool());
        CHECK(data.get("var")->is_true());
        data.set("var", "hello");
        CHECK(data.get("var")->is_string());
        CHECK(data.get("var")->string_value() == "hello");
    }

}

TEST_CASE("errors") {

    SECTION("unclosed_section") {
        mustache tmpl("test {{#employees}}");
        CHECK_FALSE(tmpl.is_valid());
        CHECK(tmpl.error_message() == "Unclosed section \"employees\" at 5");
    }

    SECTION("unclosed_section_nested") {
        mustache tmpl("{{#var1}}hello{{#var2}}world");
        data data;
        data.set("var1", data::type::bool_true);
        data.set("var2", data::type::bool_true);
        CHECK(tmpl.render(data) == "");
        CHECK(tmpl.is_valid() == false);
        CHECK(tmpl.error_message() == "Unclosed section \"var1\" at 0");
    }

    SECTION("unclosed_section_nested2") {
        mustache tmpl("{{#var1}}hello{{#var2}}world{{/var1}}");
        data data;
        data.set("var1", data::type::bool_true);
        data.set("var2", data::type::bool_true);
        CHECK(tmpl.render(data) == "");
        CHECK(tmpl.is_valid() == false);
        CHECK(tmpl.error_message() == "Unclosed section \"var1\" at 0");
    }

    SECTION("unclosed_section_in_section") {
        mustache tmpl("{{#a}}{{^b}}{{/c}}{{/a}}");
        CHECK_FALSE(tmpl.is_valid());
        CHECK(tmpl.error_message() == "Unclosed section \"b\" at 6");
    }

    SECTION("unclosed_tag") {
        mustache tmpl("test {{employees");
        CHECK_FALSE(tmpl.is_valid());
        CHECK(tmpl.error_message() == "Unclosed tag at 5");
    }

    SECTION("unopened_section") {
        mustache tmpl("test {{/employees}}");
        CHECK_FALSE(tmpl.is_valid());
        CHECK(tmpl.error_message() == "Unopened section \"employees\" at 5");
    }

    SECTION("invalid_set_delimiter") {
        std::vector<std::string> invalids;
        invalids.push_back("test {{=< =}}");  // not 5 characters
        invalids.push_back("test {{=....}}"); // not ending with =
        invalids.push_back("test {{=...=}}"); // does not contain space
        invalids.push_back("test {{=.  ==}}"); // can't contain equal sign
        invalids.push_back("test {{==  .=}}"); // can't contain equal sign
        invalids.push_back("test {{=[ ] ] ]=}}"); // can't contain space
        invalids.push_back("test {{=[ [ ]=}}"); // can't contain space
        std::vector<std::string>::size_type total = 0;
        for (const auto& str: invalids) {
            mustache tmpl(str);
            CHECK_FALSE(tmpl.is_valid());
            CHECK(tmpl.error_message() == "Invalid set delimiter tag at 5");
            ++total;
        }
        CHECK(total == invalids.size());
        CHECK(total == 7);
    }

    SECTION("lambda") {
        mustache tmpl{"Hello {{lambda}}!"};
        data dat("lambda", data{lambda{[](const std::string&){
            return "{{#what}}";
        }}});
        CHECK(tmpl.is_valid() == true);
        CHECK(tmpl.error_message() == "");
        CHECK(tmpl.render(dat) == "Hello ");
        CHECK(tmpl.is_valid() == false);
        CHECK(tmpl.error_message() == "Unclosed section \"what\" at 0");
    }

    SECTION("lambda2") {
        mustache tmpl{"Hello {{lambda}}!"};
        data dat("lambda", data{lambda{[](const std::string&){
            return "{{what}}";
        }}});
        dat["what"] = data{lambda{[](const std::string&){
            return "{{#blah}}";
        }}};
        CHECK(tmpl.is_valid() == true);
        CHECK(tmpl.error_message() == "");
        CHECK(tmpl.render(dat) == "Hello ");
        CHECK(tmpl.is_valid() == false);
        CHECK(tmpl.error_message() == "Unclosed section \"blah\" at 0");
    }

    SECTION("partial") {
        mustache tmpl{"Hello {{>partial}}!"};
        data dat("partial", data{partial{[](){
            return "{{#what}}";
        }}});
        CHECK(tmpl.is_valid() == true);
        CHECK(tmpl.error_message() == "");
        CHECK(tmpl.render(dat) == "Hello ");
        CHECK(tmpl.is_valid() == false);
        CHECK(tmpl.error_message() == "Unclosed section \"what\" at 0");
    }

    SECTION("partial2") {
        mustache tmpl{"Hello {{>partial}}!"};
        data data("partial", {partial{[](){
            return "{{what}}";
        }}});
        data["what"] = lambda{[](const std::string&){
            return "{{#blah}}";
        }};
        CHECK(tmpl.is_valid() == true);
        CHECK(tmpl.error_message() == "");
        CHECK(tmpl.render(data) == "Hello ");
        CHECK(tmpl.is_valid() == false);
        CHECK(tmpl.error_message() == "Unclosed section \"blah\" at 0");
    }

    SECTION("section_lambda") {
        mustache tmpl{"{{#what}}asdf{{/what}}"};
        data data("what", lambda{[](const std::string&){
            return "{{blah";
        }});
        CHECK(tmpl.is_valid() == true);
        CHECK(tmpl.error_message() == "");
        CHECK(tmpl.render(data) == "");
        CHECK(tmpl.is_valid() == false);
        CHECK(tmpl.error_message() == "Unclosed tag at 0");
    }

}

TEST_CASE("partials") {

    SECTION("empty") {
        mustache tmpl{"{{>header}}"};
        data data;
        CHECK(tmpl.render(data) == "");
    }

    SECTION("basic") {
        mustache tmpl{"{{>header}}"};
        partial part = []() {
            return "Hello World";
        };
        data dat("header", data{part});
        CHECK(tmpl.render(dat) == "Hello World");
    }

    SECTION("context") {
        mustache tmpl{"{{>header}}"};
        partial part{[]() {
            return "Hello {{name}}";
        }};
        data dat("header", data{part});
        dat["name"] = "Steve";
        CHECK(tmpl.render(dat) == "Hello Steve");
    }

    SECTION("nested") {
        mustache tmpl{"{{>header}}"};
        partial header{[]() {
            return "Hello {{name}} {{>footer}}";
        }};
        partial footer{[]() {
            return "Goodbye {{#names}}{{.}}|{{/names}}";
        }};
        data names{data::type::list};
        names.push_back("Jack");
        names.push_back("Jill");
        data dat("header", header);
        dat["name"] = "Steve";
        dat["footer"] = data{footer};
        dat["names"] = data{names};
        CHECK(tmpl.render(dat) == "Hello Steve Goodbye Jack|Jill|");
    }

    SECTION("dotted") {
        mustache tmpl{"{{>a.b}}"};
        partial a_b{[]() {
            return "test";
        }};
        data data("a.b", a_b);
        CHECK(tmpl.render(data) == "test");
    }
}

TEST_CASE("lambdas") {

    SECTION("basic") {
        mustache tmpl{"{{lambda}}"};
        data dat("lambda", data{lambda{[](const std::string&){
            return "Hello {{planet}}";
        }}});
        dat["planet"] = "world";
        CHECK(tmpl.render(dat) == "Hello world");
    }

    SECTION("basic_t") {
        mustache tmpl{"{{lambda}}"};
#if MUSTACHE_VS2013
        data dat("lambda", data{lambda_t{lambda_t::type1{[](const std::string&){
#else
        data dat("lambda", data{lambda_t{{[](const std::string&){
#endif
            return "Hello {{planet}}";
        }}}});
        dat["planet"] = "world";
        CHECK(tmpl.render(dat) == "Hello world");
    }

    SECTION("delimiters") {
        mustache tmpl{"{{= | | =}}Hello, (|&lambda|)!"};
        data dat("lambda", data{lambda{[](const std::string&){
            return "|planet| => {{planet}}";
        }}});
        dat["planet"] = "world";
        CHECK(tmpl.render(dat) == "Hello, (|planet| => world)!");
    }

    SECTION("nocaching") {
        mustache tmpl{"{{lambda}} == {{{lambda}}} == {{lambda}}"};
        int calls = 0;
        data dat("lambda", data{lambda{[&calls](const std::string&){
            ++calls;
            return std::to_string(calls);
        }}});
        CHECK(tmpl.render(dat) == "1 == 2 == 3");
    }

    SECTION("escape") {
        mustache tmpl{"<{{lambda}}{{{lambda}}}"};
        data dat("lambda", data{lambda{[](const std::string&){
            return ">";
        }}});
        CHECK(tmpl.render(dat) == "<&gt;>");
    }

    SECTION("section") {
        mustache tmpl{"<{{#lambda}}{{x}}{{/lambda}}>"};
        data dat("lambda", data{lambda{[](const std::string& text){
            return text == "{{x}}" ? "yes" : "no";
        }}});
        CHECK(tmpl.render(dat) == "<yes>");
    }

    SECTION("section_expansion") {
        mustache tmpl{"<{{#lambda}}-{{/lambda}}>"};
        data dat("lambda", data{lambda{[](const std::string& text){
            return text + "{{planet}}" + text;
        }}});
        dat["planet"] = "Earth";
        CHECK(tmpl.render(dat) == "<-Earth->");
    }

    SECTION("section_alternate_delimiters") {
        mustache tmpl{"{{= | | =}}<|#lambda|-|/lambda|>"};
        data dat("lambda", data{lambda{[](const std::string& text){
            return text + "{{planet}} => |planet|" + text;
        }}});
        dat["planet"] = "Earth";
        CHECK(tmpl.render(dat) == "<-{{planet}} => Earth->");
    }

    const lambda sectionLambda{[](const std::string& text){
        return "__" + text + "__";
    }};

    SECTION("section_multiple_calls") {
        mustache tmpl{"{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}"};
        data data("lambda", sectionLambda);
        CHECK(tmpl.render(data) == "__FILE__ != __LINE__");
    }

    SECTION("section_inverted") {
        mustache tmpl{"<{{^lambda}}{{static}}{{/lambda}}>"};
        data data("lambda", sectionLambda);
        data["static"] = "static";
        CHECK(tmpl.render(data) == "<>");
    }

}

TEST_CASE("dotted_names") {

    SECTION("basic") {
        mustache tmpl{"\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\""};
        data person{"name", "Joe"};
        CHECK(tmpl.render({"person", person}) == "\"Joe\" == \"Joe\"");
    }

    SECTION("triple_mustache") {
        mustache tmpl{"\"{{{person.name}}}\" == \"{{#person}}{{name}}{{/person}}\""};
        data person{"name", "Joe"};
        CHECK(tmpl.render({"person", person}) == "\"Joe\" == \"Joe\"");
    }

    SECTION("ampersand") {
        mustache tmpl{"\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\""};
        data person{"name", "Joe"};
        CHECK(tmpl.render({"person", person}) == "\"Joe\" == \"Joe\"");
    }

    SECTION("depth") {
        mustache tmpl{"\"{{a.b.c.d.e.name}}\" == \"Phil\""};
        data data{"a", {"b", {"c", {"d", {"e", {"name", "Phil"}}}}}};
        CHECK(tmpl.render(data) == "\"Phil\" == \"Phil\"");
    }

    SECTION("broken_chains1") {
        mustache tmpl{"\"{{a.b.c}}\" == \"\""};
        data data{"a", data::type::list};
        CHECK(tmpl.render(data) == "\"\" == \"\"");
    }

    SECTION("broken_chains2") {
        mustache tmpl{"\"{{a.b.c.name}}\" == \"\""};
        data data;
        data["a"] = {"b", data::type::list};
        data["c"] = {"name", "Jim"};
        CHECK(tmpl.render(data) == "\"\" == \"\"");
    }

    SECTION("depth2") {
        mustache tmpl{"\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\""};
        data data;
        data["a"] = {"b", {"c", {"d", {"e", {"name", "Phil"}}}}};
        data["b"] = {"c", {"d", {"e", {"name", "Wrong"}}}};
        CHECK(tmpl.render(data) == "\"Phil\" == \"Phil\"");
    }

    SECTION("scope") {
        mustache tmpl{"\"{{#a}}{{b.name}}{{/a}}\" == \"Phil\""};
        data data;
        data["a"] = {"x", "y"};
        data["b"] = {"name", "Phil"};
        CHECK(tmpl.render(data) == "\"Phil\" == \"Phil\"");
    }
}

TEST_CASE("bustache_benchmark") {

    // https://github.com/jamboree/bustache/blob/master/test/benchmark.cpp
    int n = 0;
    object dat
    {
        {"header", "Colors"},
        {"items",
            list
            {
                object
                {
                    {"name", "red"},
                    {"first", true},
                    {"url", "#Red"}
                },
                object
                {
                    {"name", "green"},
                    {"link", true},
                    {"url", "#Green"}
                },
                object
                {
                    {"name", "blue"},
                    {"link", true},
                    {"url", "#Blue"}
                }
            }
        },
        {"empty", false},
        {"count", lambda{[&n](const std::string&) { return std::to_string(++n); }}},
        {"array", list{"1", "2", "3"}},
        {"a", object{{"b", object{{"c", true}}}}},
        {"comments",
            list
            {
                object
                {
                    {"name", "Joe"},
                    {"body", "<html> should be escaped"}
                },
                object
                {
                    {"name", "Sam"},
                    {"body", "{{mustache}} can be seen"}
                },
                object
                {
                    {"name", "New"},
                    {"body", "break\nup"}
                }
            }
        }
    };

}

TEST_CASE("lambda_render") {

    SECTION("auto-render") {
        mustache tmpl{"{{#wrapped}}{{name}} is awesome.{{/wrapped}}"};
        data data;
        data["name"] = "Willy";
        data["wrapped"] = lambda{[](const std::string& text) {
            CHECK(text == "{{name}} is awesome.");
            return "<b>" + text + "</b>";
        }};
        CHECK(tmpl.render(data) == "<b>Willy is awesome.</b>");
    }

    SECTION("no-render") {
        mustache tmpl{"{{#wrapped}}{{name}} is awesome.{{/wrapped}}"};
        data data;
        data["name"] = "Willy";
        data["wrapped"] = lambda2{[](const std::string& text, const renderer&) {
            CHECK(text == "{{name}} is awesome.");
            return "<b>" + text + "</b>";
        }};
        CHECK(tmpl.render(data) == "<b>{{name}} is awesome.</b>");
    }

    SECTION("no-render-lambda_t") {
        mustache tmpl{"{{#wrapped}}{{name}} is awesome.{{/wrapped}}"};
        data data;
        data["name"] = "Willy";
#if MUSTACHE_VS2013
        data["wrapped"] = lambda_t{lambda_t::type2{[](const std::string& text, const renderer&) {
#else
        data["wrapped"] = lambda_t{{[](const std::string& text, const renderer&) {
#endif
            CHECK(text == "{{name}} is awesome.");
            return "<b>" + text + "</b>";
        }}};
        CHECK(tmpl.render(data) == "<b>{{name}} is awesome.</b>");
    }

    SECTION("manual-render") {
        mustache tmpl{"{{#wrapped}}{{name}} is awesome.{{/wrapped}}"};
        data data;
        data["name"] = "Willy";
        data["wrapped"] = lambda2{[](const std::string& text, const renderer& render) {
            CHECK(text == "{{name}} is awesome.");
            const auto renderedText = render(text);
            CHECK(renderedText == "Willy is awesome.");
            return "<b>" + renderedText + "</b>";
        }};
        CHECK(tmpl.render(data) == "<b>Willy is awesome.</b>");
    }

    SECTION("manual-render-lambda_t") {
        mustache tmpl{"{{#wrapped}}{{name}} is awesome.{{/wrapped}}"};
        data data;
        data["name"] = "Willy";
#if MUSTACHE_VS2013
        data["wrapped"] = lambda_t{lambda_t::type2{[](const std::string& text, const renderer& render) {
#else
        data["wrapped"] = lambda_t{{[](const std::string& text, const renderer& render) {
#endif
            CHECK(text == "{{name}} is awesome.");
            const auto renderedText = render(text);
            CHECK(renderedText == "Willy is awesome.");
            return "<b>" + renderedText + "</b>";
        }}};
        CHECK(tmpl.render(data) == "<b>Willy is awesome.</b>");
    }

    SECTION("manual-render-append-tag") {
        // When using the render lambda, any text returned should not be itself rendered.
        mustache tmpl{"{{#wrapped}}{{name}} is awesome.{{/wrapped}}"};
        data data;
        data["name"] = "Willy";
        data["wrapped"] = lambda2{[](const std::string& text, const renderer& render) {
            CHECK(text == "{{name}} is awesome.");
            const auto renderedText = render(text);
            CHECK(renderedText == "Willy is awesome.");
            return "<b>" + renderedText + "</b>Hello {{name}}.";
        }};
        CHECK(tmpl.render(data) == "<b>Willy is awesome.</b>Hello {{name}}.");
    }

    SECTION("manual-render-error") {
        mustache tmpl{"{{#wrapped}}{{name}} is awesome.{{/wrapped}}"};
        data data;
        data["name"] = "Willy";
        data["wrapped"] = lambda2{[](const std::string& text, const renderer& render) -> mustache::string_type {
            CHECK(text == "{{name}} is awesome.");
            const auto renderedText = render("{{name is awesome");
            CHECK(renderedText == "");
            return {};
        }};
        CHECK(tmpl.render(data) == "");
        CHECK_FALSE(tmpl.is_valid());
        CHECK(tmpl.error_message() == "Unclosed tag at 0");
    }

    SECTION("lambda-render-variable") {
        mustache tmpl{"{{name}} is awesome."};
        data data;
        data["name"] = lambda2{[](const std::string&, const renderer&) -> mustache::string_type {
            return {};
        }};
        CHECK(tmpl.render(data) == "");
        CHECK_FALSE(tmpl.is_valid());
        CHECK(tmpl.error_message() == "Lambda with render argument is not allowed for regular variables");
    }

    SECTION("lambda-render-bug38") {
        mustache tmpl{"It is true that {{#wrapped}}{{name}} is awesome.{{/wrapped}}"};
        data data;
        data["name"] = "Willy";
        data["wrapped"] = lambda2{[](const std::string& text, const renderer& render) {
            const auto renderedText = render(text);
            return "<b>" + renderedText + "</b>";
        }};
        CHECK(tmpl.render(data) == "It is true that <b>Willy is awesome.</b>");
    }

    SECTION("lambda-render-multiple-times") {
        mustache tmpl{"It is true that {{#wrapped}}{{name}} is awesome.{{/wrapped}}"};
        data data;
        data["name"] = "Willy";
        data["wrapped"] = lambda2{[](const std::string& text, const renderer& render) {
            CHECK(text == "{{name}} is awesome.");
            const auto pre_lambda_text = render("");
            CHECK(pre_lambda_text.empty());
            const auto renderedText = render(text);
            CHECK(renderedText == "Willy is awesome.");
            CHECK(render("").empty());
            CHECK(render(text) == "Willy is awesome.");
            return pre_lambda_text + "<b>" + renderedText + "</b>";
        }};
        CHECK(tmpl.render(data) == "It is true that <b>Willy is awesome.</b>");
    }

}

TEST_CASE("custom_escape") {

    SECTION("basic") {
        mustache tmpl("printf(\"Say {{password}} and enter\");{{&newline}}");
        tmpl.set_custom_escape([](const std::string& s) {
            std::string ret; ret.reserve(s.size());
            for (const auto ch: s) {
                switch (ch) {
                    case '\"':
                    case '\n':
                        ret.append({'\\', ch});
                        break;
                    default:
                        ret.append(1, ch);
                        break;
                }
            }
            return ret;
        });
        object data{ { "password", "\"friend\"" }, { "newline", "\n" } };
        CHECK(tmpl.render(data) == "printf(\"Say \\\"friend\\\" and enter\");\n");
    }

    SECTION("no_html_escape") {
        // make sure when using a custom escape that HTML is not escaped
        mustache tmpl("hello {{world}}");
        tmpl.set_custom_escape([](const std::string& s) {
            // doing nothing here
            return s;
        });
        object data{ { "world", "<world>" } };
        CHECK(tmpl.render(data) == "hello <world>");
    }

    SECTION("lambda") {
        mustache tmpl{"hello {{lambda}}"};
        data dat("lambda", data{lambda{[](const std::string&){
            return "\"friend\"";
        }}});
        tmpl.set_custom_escape([](const std::string& s) {
            std::string ret; ret.reserve(s.size());
            for (const auto ch: s) {
                switch (ch) {
                    case '\"':
                    case '\n':
                        ret.append({'\\', ch});
                        break;
                    default:
                        ret.append(1, ch);
                        break;
                }
            }
            return ret;
        });
        CHECK(tmpl.render(dat) == "hello \\\"friend\\\"");
    }

    SECTION("#lambda") {
        mustache tmpl{"hello {{#quote}}friend{{/quote}}"};
#if MUSTACHE_VS2013
        data dat1("quote", data{lambda_t{lambda_t::type2{[](const std::string& s, const renderer& r){
#else
        data dat1("quote", data{lambda_t{{[](const std::string& s, const renderer& r){
#endif
            return r("<\"" + s + "\">", true);
        }}}});
#if MUSTACHE_VS2013
        data dat2("quote", data{lambda_t{lambda_t::type2{[](const std::string& s, const renderer& r){
#else
        data dat2("quote", data{lambda_t{{[](const std::string& s, const renderer& r){
#endif
            return r("<\"" + s + "\">", false);
        }}}});
        tmpl.set_custom_escape([](const std::string& s) {
            std::string ret; ret.reserve(s.size());
            for (const auto ch: s) {
                switch (ch) {
                    case '\"':
                    case '\n':
                        ret.append({'\\', ch});
                        break;
                    default:
                        ret.append(1, ch);
                        break;
                }
            }
            return ret;
        });
        CHECK(tmpl.render(dat1) == "hello <\\\"friend\\\">");
        CHECK(tmpl.render(dat2) == "hello <\"friend\">");
    }

    SECTION("partial") {
        mustache tmpl{"hello {{>partial}}"};
        object dat({{"what", "\"friend\""}, {"partial", data{partial{[](){
            return "{{what}}";
        }}}}});
        tmpl.set_custom_escape([](const std::string& s) {
            std::string ret; ret.reserve(s.size());
            for (const auto ch: s) {
                switch (ch) {
                    case '\"':
                    case '\n':
                        ret.append({'\\', ch});
                        break;
                    default:
                        ret.append(1, ch);
                        break;
                }
            }
            return ret;
        });
        CHECK(tmpl.render(dat) == "hello \\\"friend\\\"");
    }

    SECTION("none") {
        mustache tmpl("hello {{what}}");
        mustache::escape_handler esc;
        tmpl.set_custom_escape(esc);
        object dat({ {"what", "\"friend\""} });
        CHECK_THROWS_AS(tmpl.render(dat), std::bad_function_call&);
    }

}

template <typename string_type>
class my_context : public basic_context<string_type> {
public:
    my_context()
        : value_("Steve")
    {
    }

    virtual void push(const basic_data<string_type>* /*data*/) override {
    }

    virtual void pop() override {
    }

    virtual const basic_data<string_type>* get(const string_type& name) const override {
        if (name == "what") {
            return &value_;
        }
        return nullptr;
    }

    virtual const basic_data<string_type>* get_partial(const string_type& /*name*/) const override {
        return nullptr;
    }

private:
    basic_data<string_type> value_;
};

TEST_CASE("custom_context") {

    SECTION("basic") {
        my_context<mustache::string_type> ctx;
        mustache tmpl("Hello {{what}}");
        std::ostream& stream = tmpl.render(ctx, std::cout) << std::endl;
        CHECK(tmpl.is_valid());
        CHECK(tmpl.error_message() == "");
        CHECK(tmpl.render(ctx) == "Hello Steve");
    }

    SECTION("empty") {
        my_context<mustache::string_type> ctx;
        mustache tmpl("Hello {{world}}");
        CHECK(tmpl.render(ctx) == "Hello ");
    }

}

template <typename string_type>
class file_partial_context : public context<string_type> {
public:
    file_partial_context(const basic_data<string_type>* data)
        : context<string_type>(data)
    {
    }

    virtual const basic_data<string_type>* get_partial(const string_type& name) const override {
        const auto cached = cached_files_.find(name);
        REQUIRE(cached == cached_files_.end());
        string_type result;
        REQUIRE(read_file(name, result));
        return &cached_files_.insert(std::make_pair(name, basic_data<string_type>(result))).first->second;
    }

private:
    bool read_file(const string_type& name, string_type& file_contents) const {
        // read from file [name].mustache (fake the data for the test)
        REQUIRE(name == "what");
        file_contents = "World";
        return true;
    }

    mutable std::unordered_map<string_type, basic_data<string_type>> cached_files_;
};

TEST_CASE("file_partial_context") {

    data dat("punctuation", "!");
    file_partial_context<mustache::string_type> ctx{&dat};
    mustache tmpl("Hello {{>what}}{{punctuation}}");
    CHECK(tmpl.render(ctx) == "Hello World!");

}

TEST_CASE("standalone_lines") {

    SECTION("parse_whitespace_basic") {
        const mustache::string_type input = "\n\r\n\t \n\n\r";
        component<mustache::string_type> root_component;
        mustache::string_type error_message;
        context<mustache::string_type> ctx;
        context_internal<mustache::string_type> context{ctx};
        parser<mustache::string_type>{input, context, root_component, error_message};
        CHECK(error_message.empty());
        const auto& root_children = root_component.children;
        const std::vector<mustache::string_type> text_components{"\n", "\r\n", "\t", " ", "\n", "\n", "\r"};
        REQUIRE(root_component.children.size() == 7);
        REQUIRE(root_component.children.size() == text_components.size());
        std::vector<mustache::string_type>::size_type i = 0;
        for (const auto& child : root_component.children) {
            CHECK(child.text == text_components[i++]);
            CHECK(child.tag.type == tag_type::text);
            CHECK(child.children.empty());
        }
        CHECK(root_component.children[0].is_newline());
        CHECK_FALSE(root_component.children[0].is_non_newline_whitespace());
        CHECK(root_component.children[1].is_newline());
        CHECK_FALSE(root_component.children[1].is_non_newline_whitespace());
        CHECK_FALSE(root_component.children[2].is_newline());
        CHECK(root_component.children[2].is_non_newline_whitespace());
        CHECK_FALSE(root_component.children[3].is_newline());
        CHECK(root_component.children[3].is_non_newline_whitespace());
        CHECK(root_component.children[4].is_newline());
        CHECK_FALSE(root_component.children[4].is_non_newline_whitespace());
        CHECK(root_component.children[5].is_newline());
        CHECK_FALSE(root_component.children[5].is_non_newline_whitespace());
        CHECK(root_component.children[6].is_newline());
        CHECK_FALSE(root_component.children[6].is_non_newline_whitespace());
    }

    SECTION("parse_whitespace") {
        const mustache::string_type input =
        "|\n"
        "| This Is\n"
        "{{#boolean}}\n"
        "|\n"
        "{{/boolean}}\n"
        "| A Line";
        component<mustache::string_type> root_component;
        mustache::string_type error_message;
        context<mustache::string_type> ctx;
        context_internal<mustache::string_type> context{ctx};
        parser<mustache::string_type>{input, context, root_component, error_message};
        CHECK(error_message.empty());
        const auto& root_children = root_component.children;
        REQUIRE(root_children.size() == 15);
        CHECK(root_children[0].text == "|");
        CHECK(root_children[0].tag.type == tag_type::text);
        CHECK(root_children[0].children.empty());
        CHECK(root_children[1].text == "\n");
        CHECK(root_children[1].tag.type == tag_type::text);
        CHECK(root_children[1].children.empty());
        CHECK(root_children[2].text == "|");
        CHECK(root_children[2].tag.type == tag_type::text);
        CHECK(root_children[2].children.empty());
        CHECK(root_children[3].text == " ");
        CHECK(root_children[3].tag.type == tag_type::text);
        CHECK(root_children[3].children.empty());
        CHECK(root_children[4].text == "This");
        CHECK(root_children[4].tag.type == tag_type::text);
        CHECK(root_children[4].children.empty());
        CHECK(root_children[5].text == " ");
        CHECK(root_children[5].tag.type == tag_type::text);
        CHECK(root_children[5].children.empty());
        CHECK(root_children[6].text == "Is");
        CHECK(root_children[6].tag.type == tag_type::text);
        CHECK(root_children[6].children.empty());
        CHECK(root_children[7].text == "\n");
        CHECK(root_children[7].tag.type == tag_type::text);
        CHECK(root_children[7].children.empty());
        CHECK(root_children[8].text.empty());
        CHECK(root_children[8].tag.type == tag_type::section_begin);
        REQUIRE(root_children[8].children.size() == 3);
        CHECK(root_children[8].children[0].text == "\n");
        CHECK(root_children[8].children[0].tag.type == tag_type::text);
        CHECK(root_children[8].children[0].children.empty());
        CHECK(root_children[8].children[1].text == "|");
        CHECK(root_children[8].children[1].tag.type == tag_type::text);
        CHECK(root_children[8].children[1].children.empty());
        CHECK(root_children[8].children[2].text == "\n");
        CHECK(root_children[8].children[2].tag.type == tag_type::text);
        CHECK(root_children[8].children[2].children.empty());
        CHECK(root_children[9].text == "\n");
        CHECK(root_children[9].tag.type == tag_type::text);
        CHECK(root_children[9].children.empty());
        CHECK(root_children[10].text == "|");
        CHECK(root_children[10].tag.type == tag_type::text);
        CHECK(root_children[10].children.empty());
        CHECK(root_children[11].text == " ");
        CHECK(root_children[11].tag.type == tag_type::text);
        CHECK(root_children[11].children.empty());
        CHECK(root_children[12].text == "A");
        CHECK(root_children[12].tag.type == tag_type::text);
        CHECK(root_children[12].children.empty());
        CHECK(root_children[13].text == " ");
        CHECK(root_children[13].tag.type == tag_type::text);
        CHECK(root_children[13].children.empty());
        CHECK(root_children[14].text == "Line");
        CHECK(root_children[14].tag.type == tag_type::text);
        CHECK(root_children[14].children.empty());
    }

    SECTION("remove_standalone_lines") {
        mustache tmpl{
            "|\n"
            "| This Is\n"
            "{{#boolean}}\n"
            "|\n"
            "{{/boolean}}\n"
            "| A Line"
        };
        data data("boolean", true);
        CHECK(tmpl.render(data) ==
            "|\n"
            "| This Is\n"
            "|\n"
            "| A Line"
        );
    }

    SECTION("remove_indented_standalone_lines") {
        mustache tmpl{
            "|\n"
            "| This Is\n"
            "  {{#boolean}}\n"
            "|\n"
            "  {{/boolean}}\n"
            "| A Line"
        };
        data data("boolean", true);
        CHECK(tmpl.render(data) ==
            "|\n"
            "| This Is\n"
            "|\n"
            "| A Line"
        );
    }

    SECTION("crlf") {
        mustache tmpl{"|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|"};
        data data("boolean", true);
        CHECK(tmpl.render(data) == "|\r\n|");
    }

    SECTION("without_previous_line") {
        mustache tmpl{"  {{#boolean}}\n#{{/boolean}}\n/"};
        data data("boolean", true);
        CHECK(tmpl.render(data) == "#\n/");
    }

    SECTION("without_next_newline") {
        mustache tmpl{"#{{#boolean}}\n/\n  {{/boolean}}"};
        data data("boolean", true);
        CHECK(tmpl.render(data) == "#\n/\n");
    }

    SECTION("section_list") {
        mustache tmpl{
            "Text1\n"
            "{{#section}}\n"
            "Text2\n"
            "{{/section}}\n"
            "Text3\n"
        };
        const list section{
            "Text2",
            "Text2",
            "Text2",
        };
        CHECK(tmpl.render(data{"section", section}) ==
            "Text1\n"
            "Text2\n"
            "Text2\n"
            "Text2\n"
            "Text3\n"
        );
    }

    SECTION("section_list_partial_inline") {
        mustache tmpl{
            "Text1\n"
            "{{#section}}blah\n"
            "{{/section}}\n"
            "Text3\n"
        };
        const list section{
            "Text2",
            "Text2",
            "Text2",
        };
        CHECK(tmpl.render(data{"section", section}) ==
            "Text1\n"
            "blah\n"
            "blah\n"
            "blah\n"
            "Text3\n"
        );
    }

    SECTION("section_list_full_inline") {
        mustache tmpl{
            "Text1\n"
            "{{#section}}blah{{/section}}\n"
            "Text3\n"
        };
        const list section{
            "Text2",
            "Text2",
            "Text2",
        };
        CHECK(tmpl.render(data{"section", section}) ==
            "Text1\n"
            "blahblahblah\n"
            "Text3\n"
        );
    }

    SECTION("section_list_not_empty_lines") {
        mustache tmpl{
            "Text1\n"
            "{{#section}}a\n"
            "test {{.}}\n"
            "{{/section}}b\n"
            "Text3\n"
        };
        const list section{
            "a",
            "b",
            "c",
        };
        CHECK(tmpl.render(data{"section", section}) ==
            "Text1\n"
            "a\n"
            "test a\n"
            "a\n"
            "test b\n"
            "a\n"
            "test c\n"
            "b\n"
            "Text3\n"
        );
    }

    SECTION("partial_indent_bug") {
        mustache tmpl{
            "No indent\n"
            "    Indent\n"
            "    {{>partial1}}Indent\n"
            "    {{>partial2}}Indent\n"
            "    {{>partial3}}    Indent\n"
            "    {{>partial4}}    Indent\n"
            "    {{>partial5}}\n"
            "No indent\n"
        };
        data dat;
        dat.set("partial1", partial{[]{
            return "{{#section}}{{/section}}";
        }});
        dat.set("partial2", partial{[]{
            return "{{#invalidSection}}{{/invalidSection}}";
        }});
        dat.set("partial3", partial{[]{
            return "    {{#section}}Indent more{{/section}}\n";
        }});
        dat.set("partial4", partial{[]{
            // produces a whitespace-only line, which gets removed, then a "Hello" line
            return "    {{#section}}{{/section}}\n    Hello\n";
        }});
        dat.set("partial5", partial{[]{
            // produces whitespace-only, which gets removed because parent line is also whitespace only
            return "    {{#section}}{{/section}}";
        }});
        dat.set("section", data::type::bool_true);
        CHECK(tmpl.render(dat) ==
            "No indent\n"
            "    Indent\n"
            "    Indent\n"
            "    Indent\n"
            "        Indent more\n"
            "    Indent\n"
            "    Hello\n"
            "    Indent\n"
            "No indent\n"
        );
    }

}

