From 1d0d1bde8f9f7ced813aa0186bbfa7737b7d9367 Mon Sep 17 00:00:00 2001 From: Daniel Sipka Date: Mon, 4 May 2015 16:14:32 +0200 Subject: [PATCH] specs compliant lambdas --- README.md | 11 ++++--- include/mstch/mstch.hpp | 15 ++++------ src/state/outside_section.cpp | 4 +-- src/visitor/render_node.hpp | 9 ++++-- src/visitor/render_section.hpp | 4 +-- test/data/higher_order_sections.hpp | 4 +-- test/data/higher_order_sections.txt | 2 +- test/data/nested_higher_order_sections.hpp | 4 +-- test/data/section_functions_in_partials.hpp | 4 +-- test/headerize.cpp | 4 ++- test/specs/~lambdas.json | 1 + test/specs_lambdas.hpp | 32 +++++++++++++++++++++ test/test_main.cpp | 5 ++++ 13 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 test/specs/~lambdas.json create mode 100644 test/specs_lambdas.hpp diff --git a/README.md b/README.md index 6756aaa..7a1a4ea 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ mstch is a complete implementation of [{{mustache}}](http://mustache.github.io/) templates using modern C++. It's compliant with [specifications](https://github.com/mustache/spec) -v1.1.2. +v1.1.2, including the lambda module. [![Build Status](https://travis-ci.org/no1msd/mstch.svg?branch=master)](https://travis-ci.org/no1msd/mstch) @@ -125,16 +125,15 @@ Output: Hello World! ``` -Or it accepts a `const std::string&` and a `mstch::renderer`. The first one is -passed the unrendered literal block, the second is a `std::function` that can be -called to render it: +Or it accepts a `const std::string&` that gets the unrendered literal block. +The returned string will be parsed in both cases: ```c++ std::string view{"{{#bold}}{{yay}} :){{/bold}}"}; mstch::map context{ {"yay", std::string{"Yay!"}}, - {"bold", mstch::lambda{[](const std::string& text, mstch::renderer render) { - return "" + render(text) + ""; + {"bold", mstch::lambda{[](const std::string& text) { + return "" + text + ""; }}} }; diff --git a/include/mstch/mstch.hpp b/include/mstch/mstch.hpp index f9b42d4..b5675f6 100644 --- a/include/mstch/mstch.hpp +++ b/include/mstch/mstch.hpp @@ -9,8 +9,6 @@ namespace mstch { -using renderer = std::function; - namespace internal { template @@ -47,7 +45,7 @@ class is_fun { template static fun_without_args& test( really_has*); template static fun_with_args& test( - really_has*); template static not_fun& test(...); @@ -62,7 +60,7 @@ class lambda { public: template lambda(F f, typename std::enable_if::no_args>::type* =0): - fun([f](const std::string&,renderer){return f();}) + fun([f](const std::string&){return f();}) { } @@ -72,15 +70,12 @@ class lambda { { } - std::string operator()( - const std::string& text = "", - renderer renderer = [](const std::string&){ return "";}) const - { - return fun(text, renderer); + std::string operator()(const std::string& text = "") const { + return fun(text); } private: - std::function fun; + std::function fun; }; using node = boost::make_recursive_variant< diff --git a/src/state/outside_section.cpp b/src/state/outside_section.cpp index c6080e1..c165f9b 100644 --- a/src/state/outside_section.cpp +++ b/src/state/outside_section.cpp @@ -18,9 +18,9 @@ std::string outside_section::render( ctx.set_state(in_section::type::inverted, token.name()); break; case token::type::variable: - return visit(render_node(flag::escape_html), ctx.get_node(token.name())); + return visit(render_node(ctx, flag::escape_html), ctx.get_node(token.name())); case token::type::unescaped_variable: - return visit(render_node(flag::none), ctx.get_node(token.name())); + return visit(render_node(ctx, flag::none), ctx.get_node(token.name())); case token::type::text: return token.raw(); case token::type::partial: diff --git a/src/visitor/render_node.hpp b/src/visitor/render_node.hpp index 0980efd..97037a6 100644 --- a/src/visitor/render_node.hpp +++ b/src/visitor/render_node.hpp @@ -3,6 +3,7 @@ #include #include +#include "render_context.hpp" #include "mstch/mstch.hpp" #include "utils.hpp" @@ -11,7 +12,9 @@ namespace mstch { class render_node: public boost::static_visitor { public: enum class flag { none, escape_html }; - render_node(flag p_flag = flag::none): m_flag(p_flag) { + render_node(render_context& ctx, flag p_flag = flag::none): + ctx(ctx), m_flag(p_flag) + { } template @@ -34,7 +37,8 @@ class render_node: public boost::static_visitor { } std::string operator()(const lambda& value) const { - return (m_flag == flag::escape_html) ? html_escape(value()) : value(); + auto rendered = render_context::push(ctx).render(template_type{value()}); + return (m_flag == flag::escape_html) ? html_escape(rendered) : rendered; } std::string operator()(const std::string& value) const { @@ -42,6 +46,7 @@ class render_node: public boost::static_visitor { } private: + render_context& ctx; flag m_flag; }; diff --git a/src/visitor/render_section.hpp b/src/visitor/render_section.hpp index d28b7b1..be26435 100644 --- a/src/visitor/render_section.hpp +++ b/src/visitor/render_section.hpp @@ -29,9 +29,7 @@ class render_section: public boost::static_visitor { for(auto& token: section) section_str += token.raw(); - return fun(section_str, [this](const std::string& str) { - return render_context::push(ctx).render(template_type{str}); - }); + return render_context::push(ctx).render(template_type{fun(section_str)}); } std::string operator()(const array& array) const { diff --git a/test/data/higher_order_sections.hpp b/test/data/higher_order_sections.hpp index 75599a4..5a81935 100644 --- a/test/data/higher_order_sections.hpp +++ b/test/data/higher_order_sections.hpp @@ -19,8 +19,8 @@ class higher_order_sections: public mstch::object { } mstch::node bolder() { - return mstch::lambda{[this](const std::string& text, mstch::renderer render) { - return text + " => " + render(text) + " " + m_helper; + return mstch::lambda{[this](const std::string& text) { + return "" + text + " " + m_helper; }}; } }; diff --git a/test/data/higher_order_sections.txt b/test/data/higher_order_sections.txt index be50ad7..9db786a 100644 --- a/test/data/higher_order_sections.txt +++ b/test/data/higher_order_sections.txt @@ -1 +1 @@ -Hi {{name}}. => Hi Tater. To tinker? +Hi Tater. To tinker? diff --git a/test/data/nested_higher_order_sections.hpp b/test/data/nested_higher_order_sections.hpp index 28acc89..fc56a4a 100644 --- a/test/data/nested_higher_order_sections.hpp +++ b/test/data/nested_higher_order_sections.hpp @@ -1,6 +1,6 @@ const mstch::node nested_higher_order_sections_data = mstch::map{ - {"bold", mstch::lambda{[](const std::string& text, mstch::renderer render) { - return std::string{""} + render(text) + std::string{""}; + {"bold", mstch::lambda{[](const std::string& text) { + return std::string{""} + text + std::string{""}; }}}, {"person", mstch::map{{"name", std::string{"Jonas"}}}} }; \ No newline at end of file diff --git a/test/data/section_functions_in_partials.hpp b/test/data/section_functions_in_partials.hpp index 8f60fd5..57fa9b5 100644 --- a/test/data/section_functions_in_partials.hpp +++ b/test/data/section_functions_in_partials.hpp @@ -1,5 +1,5 @@ const mstch::node section_functions_in_partials_data = mstch::map{ - {"bold", mstch::lambda{[](const std::string& text, mstch::renderer render) { - return std::string{""} + render(text) + std::string{""}; + {"bold", mstch::lambda{[](const std::string& text) { + return std::string{""} + text + std::string{""}; }}} }; \ No newline at end of file diff --git a/test/headerize.cpp b/test/headerize.cpp index d8b8750..c23e946 100644 --- a/test/headerize.cpp +++ b/test/headerize.cpp @@ -56,7 +56,9 @@ int main(int argc, char* argv[]) { if (vm.count("input-string")) for (auto& filename: vm["input-string"].as>()) { std::ifstream input(filename, std::ios::in); - wrap_string(input, output, boost::replace_all_copy(filename, ".", "_")); + std::string variable_name = boost::replace_all_copy(filename, ".", "_"); + boost::replace_all(variable_name, "~", ""); + wrap_string(input, output, variable_name); input.close(); } diff --git a/test/specs/~lambdas.json b/test/specs/~lambdas.json new file mode 100644 index 0000000..eac6429 --- /dev/null +++ b/test/specs/~lambdas.json @@ -0,0 +1 @@ +{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Lambdas are a special-cased data type for use in interpolations and\nsections.\n\nWhen used as the data value for an Interpolation tag, the lambda MUST be\ntreatable as an arity 0 function, and invoked as such. The returned value\nMUST be rendered, then interpolated in place of the lambda.\n\nWhen used as the data value for a Section tag, the lambda MUST be treatable\nas an arity 1 function, and invoked as such (passing a String containing the\nunprocessed section contents). The returned value MUST be rendered, then\ninterpolated in place of the section.\n","tests":[{"name":"Interpolation","data":{"lambda":{"php":"return \"world\";","__tag__":"code","perl":"sub { \"world\" }","python":"lambda: \"world\"","ruby":"proc { \"world\" }","js":"function() { return \"world\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be interpolated."},{"name":"Interpolation - Expansion","data":{"planet":"world","lambda":{"php":"return \"{{planet}}\";","__tag__":"code","perl":"sub { \"{{planet}}\" }","python":"lambda: \"{{planet}}\"","ruby":"proc { \"{{planet}}\" }","js":"function() { return \"{{planet}}\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be parsed."},{"name":"Interpolation - Multiple Calls","data":{"lambda":{"php":"global $calls; return ++$calls;","__tag__":"code","perl":"sub { no strict; $calls += 1 }","python":"lambda: globals().update(calls=globals().get(\"calls\",0)+1) or calls","ruby":"proc { $calls ||= 0; $calls += 1 }","js":"function() { return (g=(function(){return this})()).calls=(g.calls||0)+1 }"}},"expected":"1 == 2 == 3","template":"{{lambda}} == {{{lambda}}} == {{lambda}}","desc":"Interpolated lambdas should not be cached."},{"name":"Escaping","data":{"lambda":{"php":"return \">\";","__tag__":"code","perl":"sub { \">\" }","python":"lambda: \">\"","ruby":"proc { \">\" }","js":"function() { return \">\" }"}},"expected":"<>>","template":"<{{lambda}}{{{lambda}}}","desc":"Lambda results should be appropriately escaped."},{"name":"Section","data":{"x":"Error!","lambda":{"php":"return ($text == \"{{x}}\") ? \"yes\" : \"no\";","__tag__":"code","perl":"sub { $_[0] eq \"{{x}}\" ? \"yes\" : \"no\" }","python":"lambda text: text == \"{{x}}\" and \"yes\" or \"no\"","ruby":"proc { |text| text == \"{{x}}\" ? \"yes\" : \"no\" }","js":"function(txt) { return (txt == \"{{x}}\" ? \"yes\" : \"no\") }"}},"expected":"","template":"<{{#lambda}}{{x}}{{/lambda}}>","desc":"Lambdas used for sections should receive the raw section string."},{"name":"Section - Expansion","data":{"planet":"Earth","lambda":{"php":"return $text . \"{{planet}}\" . $text;","__tag__":"code","perl":"sub { $_[0] . \"{{planet}}\" . $_[0] }","python":"lambda text: \"%s{{planet}}%s\" % (text, text)","ruby":"proc { |text| \"#{text}{{planet}}#{text}\" }","js":"function(txt) { return txt + \"{{planet}}\" + txt }"}},"expected":"<-Earth->","template":"<{{#lambda}}-{{/lambda}}>","desc":"Lambdas used for sections should have their results parsed."},{"name":"Section - Multiple Calls","data":{"lambda":{"php":"return \"__\" . $text . \"__\";","__tag__":"code","perl":"sub { \"__\" . $_[0] . \"__\" }","python":"lambda text: \"__%s__\" % (text)","ruby":"proc { |text| \"__#{text}__\" }","js":"function(txt) { return \"__\" + txt + \"__\" }"}},"expected":"__FILE__ != __LINE__","template":"{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}","desc":"Lambdas used for sections should not be cached."},{"name":"Inverted Section","data":{"static":"static","lambda":{"php":"return false;","__tag__":"code","perl":"sub { 0 }","python":"lambda text: 0","ruby":"proc { |text| false }","js":"function(txt) { return false }"}},"expected":"<>","template":"<{{^lambda}}{{static}}{{/lambda}}>","desc":"Lambdas used for inverted sections should be considered truthy."}]} \ No newline at end of file diff --git a/test/specs_lambdas.hpp b/test/specs_lambdas.hpp new file mode 100644 index 0000000..6749318 --- /dev/null +++ b/test/specs_lambdas.hpp @@ -0,0 +1,32 @@ +std::map> specs_lambdas { + {"Interpolation", [](const std::string&) { + return "world"; + }}, + {"Interpolation - Expansion", [](const std::string&) { + return "{{planet}}"; + }}, + {"Interpolation - Alternate Delimiters", [](const std::string&) { + return "|planet| => {{planet}}"; + }}, + {"Interpolation - Multiple Calls", [](const std::string&) { + static int calls = 0; return std::to_string(++calls); + }}, + {"Escaping", [](const std::string&) { + return ">"; + }}, + {"Section", [](const std::string& txt) { + return (txt == "{{x}}") ? "yes" : "no"; + }}, + {"Section - Expansion", [](const std::string& txt) { + return txt + "{{planet}}" + txt; + }}, + {"Section - Alternate Delimiters", [](const std::string& txt) { + return txt + "{{planet}} => |planet|" + txt; + }}, + {"Section - Multiple Calls", [](const std::string& txt) { + return "__" + txt + "__"; + }}, + {"Inverted Section", [](const std::string& txt) { + return ""; + }} +}; \ No newline at end of file diff --git a/test/test_main.cpp b/test/test_main.cpp index b651d61..980ae4c 100644 --- a/test/test_main.cpp +++ b/test/test_main.cpp @@ -5,6 +5,7 @@ #include "mstch/mstch.hpp" #include "test_data.hpp" #include "specs_data.hpp" +#include "specs_lambdas.hpp" using namespace mstchtest; @@ -25,6 +26,9 @@ using namespace mstchtest; if(test.count("partials")) \ for(auto& partial_item: get(test["partials"])) \ partials.insert(std::make_pair(partial_item.first, get(partial_item.second))); \ + for(auto& data_item: get(test["data"])) \ + if(data_item.first == "lambda") \ + data_item.second = mstch::lambda{specs_lambdas[get(test["name"])]}; \ SECTION(get(test["name"])) \ REQUIRE(mstch::render( \ get(test["template"]), \ @@ -98,3 +102,4 @@ SPECS_TEST(interpolation) SPECS_TEST(inverted) SPECS_TEST(partials) SPECS_TEST(sections) +SPECS_TEST(lambdas)