2024-08-05 08:13:00 +00:00

1505 lines
48 KiB
C++

/*
* 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"
);
}
}