lmi-commits
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[lmi-commits] [lmi] master 173cc28 022/156: Change interpolated strings


From: Greg Chicares
Subject: [lmi-commits] [lmi] master 173cc28 022/156: Change interpolated strings syntax to be Mustache-like
Date: Tue, 30 Jan 2018 17:21:58 -0500 (EST)

branch: master
commit 173cc288e5e12d1fbfc48e006dfa6f817c2e4568
Author: Vadim Zeitlin <address@hidden>
Commit: Vadim Zeitlin <address@hidden>

    Change interpolated strings syntax to be Mustache-like
    
    Use a subset of Mustache syntax to implement support for conditionals
    and negated conditionals which are too useful nd convenient to not
    provide them.
    
    Using a relatively widespread syntax instead of a custom one seems
    advantageous and, at any rate, doesn't seem to have any drawbacks.
---
 interpolate_string.cpp      | 154 ++++++++++++++++++++++++++++++++++++++++----
 interpolate_string.hpp      |  21 ++++--
 interpolate_string_test.cpp |  94 ++++++++++++++++++++++++---
 ledger_pdf_generator_wx.cpp |  59 +++++++----------
 4 files changed, 266 insertions(+), 62 deletions(-)

diff --git a/interpolate_string.cpp b/interpolate_string.cpp
index dae1f02..0269f78 100644
--- a/interpolate_string.cpp
+++ b/interpolate_string.cpp
@@ -25,7 +25,9 @@
 
 #include "alert.hpp"
 
+#include <stack>
 #include <stdexcept>
+#include <vector>
 
 std::string interpolate_string
     (char const* s
@@ -40,39 +42,157 @@ std::string interpolate_string
     // any better than this.
     out.reserve(strlen(s));
 
+    // The stack contains all the sections that we're currently in.
+    struct section_info
+    {
+        section_info(std::string const& name, bool active)
+            :name_(name)
+            ,active_(active)
+        {
+        }
+
+        // Name of the section, i.e. the part after "#".
+        //
+        // TODO: In C++14 this could be replaced with string_view which would
+        // save on memory allocations without compromising safety, as we know
+        // that the input string doesn't change during this function execution.
+        std::string const name_;
+
+        // If true, output section contents, otherwise simply eat it.
+        bool const active_;
+
+        // Note: we could also store the position of the section start here to
+        // improve error reporting. Currently this is done as templates we use
+        // are small and errors shouldn't be difficult to find even without the
+        // exact position, but this could change in the future.
+    };
+    std::stack<section_info, std::vector<section_info>> sections;
+
+    // Check if the output is currently active or suppressed because we're
+    // inside an inactive section.
+    auto const is_active = [&sections]()
+        {
+            return sections.empty() || sections.top().active_;
+        };
+
     for(char const* p = s; *p; ++p)
         {
-        if(p[0] == '$' && p[1] == '{')
+        // As we know that the string is NUL-terminated, it is safe to check
+        // the next character.
+        if(p[0] == '{' && p[1] == '{')
             {
             std::string name;
+            auto const pos_start = p - s + 1;
             for(p += 2;; ++p)
                 {
                 if(*p == '\0')
                     {
                     alarum()
                         << "Unmatched opening brace at position "
-                        << (p - s - 1 - name.length())
+                        << pos_start
                         << std::flush
                         ;
                     }
 
-                if(*p == '}')
+                if(p[0] == '}' && p[1] == '}')
                     {
-                    // We don't check here if name is not empty, as there is no
-                    // real reason to do it. Empty variable name may seem
-                    // strange, but why not allow using "${}" to insert
-                    // something into the interpolated string, after all?
-                    out += lookup(name);
+                    switch(name.empty() ? '\0' : name[0])
+                        {
+                        case '#':
+                        case '^':
+                            {
+                            auto const real_name = name.substr(1);
+                            // If we're inside a disabled section, it doesn't
+                            // matter whether this one is active or not.
+                            bool active = is_active();
+                            if(active)
+                                {
+                                auto const value = lookup(real_name);
+                                if(value == "1")
+                                    {
+                                    active = true;
+                                    }
+                                else if(value == "0")
+                                    {
+                                    active = false;
+                                    }
+                                else
+                                    {
+                                    alarum()
+                                        << "Invalid value '"
+                                        << value
+                                        << "' of section '"
+                                        << real_name
+                                        << "' at position "
+                                        << pos_start
+                                        << ", only \"0\" or \"1\" allowed"
+                                        << std::flush
+                                        ;
+                                    }
+
+                                if(name[0] == '^')
+                                    {
+                                    active = !active;
+                                    }
+                                }
+
+                            sections.emplace(real_name, active);
+                            }
+                            break;
+
+                        case '/':
+                            if(sections.empty())
+                                {
+                                alarum()
+                                    << "Unexpected end of section '"
+                                    << name.substr(1)
+                                    << "' at position "
+                                    << pos_start
+                                    << " without previous section start"
+                                    << std::flush
+                                    ;
+                                }
+                            if(name.compare(1, std::string::npos, 
sections.top().name_) != 0)
+                                {
+                                alarum()
+                                    << "Unexpected end of section '"
+                                    << name.substr(1)
+                                    << "' at position "
+                                    << pos_start
+                                    << " while inside the section '"
+                                    << sections.top().name_
+                                    << "'"
+                                    << std::flush
+                                    ;
+                                }
+                            sections.pop();
+                            break;
+
+                        default:
+                            if(is_active())
+                                {
+                                // We don't check here if name is not empty, as
+                                // there is no real reason to do it. Empty
+                                // variable name may seem strange, but why not
+                                // allow using "{{}}" to insert something into
+                                // the interpolated string, after all?
+                                out += lookup(name);
+                                }
+                        }
+
+                    // We consume two characters here ("}}"), not one, as in a
+                    // usual loop iteration.
+                    ++p;
                     break;
                     }
 
-                if(p[0] == '$' && p[1] == '{')
+                if(p[0] == '{' && p[1] == '{')
                     {
                     // We don't allow nested interpolations, so this can only
-                    // be result of an error, e.g. a forgotten '}' before it.
+                    // be result of an error, e.g. a forgotten "}}" somewhere.
                     alarum()
                         << "Unexpected nested interpolation at position "
-                        << (p - s + 1)
+                        << pos_start
                         << " (outer interpolation starts at "
                         << (p - s - 1 - name.length())
                         << ")"
@@ -86,11 +206,21 @@ std::string interpolate_string
                 name += *p;
                 }
             }
-        else
+        else if(is_active())
             {
             out += *p;
             }
         }
 
+    if(!sections.empty())
+        {
+        alarum()
+            << "Unclosed section '"
+            << sections.top().name_
+            << "'"
+            << std::flush
+            ;
+        }
+
     return out;
 }
diff --git a/interpolate_string.hpp b/interpolate_string.hpp
index 66c7445..e935a7a 100644
--- a/interpolate_string.hpp
+++ b/interpolate_string.hpp
@@ -29,12 +29,23 @@
 
 /// Interpolate string containing embedded variable references.
 ///
-/// Return the input string after replacing all ${variable} references in it
-/// with the value of the variable as returned by the provided function.
+/// Return the input string after replacing all {{variable}} references in it
+/// with the value of the variable as returned by the provided function. The
+/// syntax is a (strict) subset of Mustache templates, the following features
+/// are supported:
+///  - Simple variable expansion for {{variable}}.
+///  - Conditional expansion using {{#variable}}...{{/variable}}.
+///  - Negated checks of the form {{^variable}}...{{/variable}}.
 ///
-/// To allow embedding literal "${" fragment into the returned string, create a
-/// pseudo-variable returning these characters as its expansion, there is no
-/// built-in way to escape these characters.
+/// The following features are explicitly _not_ supported:
+///  - HTML escaping: this is done by a separate html::text class.
+///  - Separate types: 0/1 is false/true, anything else is an error.
+///  - Lists/section iteration (not needed yet).
+///  - Lambdas, partials, comments, delimiter changes: omitted for simplicity.
+///
+/// To allow embedding literal "{{" fragment into the returned string, create a
+/// pseudo-variable expanding to these characters as its expansion, there is no
+/// built-in way to escape them.
 ///
 /// Throw if the lookup function throws or if the string uses invalid syntax.
 std::string interpolate_string
diff --git a/interpolate_string_test.cpp b/interpolate_string_test.cpp
index 24e2b6d..e12d931 100644
--- a/interpolate_string_test.cpp
+++ b/interpolate_string_test.cpp
@@ -33,29 +33,104 @@ int test_main(int, char*[])
         };
 
     // Check that basic interpolation works.
-    BOOST_TEST_EQUAL( test_interpolate(""),             "" );
-    BOOST_TEST_EQUAL( test_interpolate("${foo}"),       "foo" );
-    BOOST_TEST_EQUAL( test_interpolate("${foo}bar"),    "foobar" );
-    BOOST_TEST_EQUAL( test_interpolate("foo${}bar"),    "foobar" );
-    BOOST_TEST_EQUAL( test_interpolate("foo${bar}"),    "foobar" );
-    BOOST_TEST_EQUAL( test_interpolate("${foo}${bar}"), "foobar" );
+    BOOST_TEST_EQUAL( test_interpolate(""),               ""        );
+    BOOST_TEST_EQUAL( test_interpolate("literal"),        "literal" );
+    BOOST_TEST_EQUAL( test_interpolate("{{foo}}"),        "foo"     );
+    BOOST_TEST_EQUAL( test_interpolate("{{foo}}bar"),     "foobar"  );
+    BOOST_TEST_EQUAL( test_interpolate("foo{{}}bar"),     "foobar"  );
+    BOOST_TEST_EQUAL( test_interpolate("foo{{bar}}"),     "foobar"  );
+    BOOST_TEST_EQUAL( test_interpolate("{{foo}}{{bar}}"), "foobar"  );
+
+    // Sections.
+    auto const section_test = [](char const* s)
+        {
+        return interpolate_string
+            (s
+            ,[](std::string const& s) -> std::string
+                {
+                if(s == "var0") return "0";
+                if(s == "var1") return "1";
+                if(s == "var" ) return "" ;
+
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            );
+        };
+
+    BOOST_TEST_EQUAL( section_test("x{{#var1}}y{{/var1}}z"),   "xyz"    );
+    BOOST_TEST_EQUAL( section_test("x{{#var0}}y{{/var0}}z"),   "xz"     );
+    BOOST_TEST_EQUAL( section_test("x{{^var0}}y{{/var0}}z"),   "xyz"    );
+    BOOST_TEST_EQUAL( section_test("x{{^var1}}y{{/var1}}z"),   "xz"     );
+
+    BOOST_TEST_EQUAL
+        (section_test("a{{#var1}}b{{#var1}}c{{/var1}}d{{/var1}}e")
+        ,"abcde"
+        );
+    BOOST_TEST_EQUAL
+        (section_test("a{{#var1}}b{{#var0}}c{{/var0}}d{{/var1}}e")
+        ,"abde"
+        );
+    BOOST_TEST_EQUAL
+        (section_test("a{{^var1}}b{{#var0}}c{{/var0}}d{{/var1}}e")
+        ,"ae"
+        );
+    BOOST_TEST_EQUAL
+        (section_test("a{{^var1}}b{{^var0}}c{{/var0}}d{{/var1}}e")
+        ,"ae"
+        );
+
+    // Some special cases.
+    BOOST_TEST_EQUAL
+        (interpolate_string
+            ("{{expanded}}"
+            ,[](std::string const& s) -> std::string
+                {
+                if(s == "expanded")
+                    {
+                    return "{{unexpanded}}";
+                    }
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            )
+        ,"{{unexpanded}}"
+        );
 
     // Should throw if the input syntax is invalid.
     BOOST_TEST_THROW
-        (test_interpolate("${x")
+        (test_interpolate("{{x")
         ,std::runtime_error
         ,lmi_test::what_regex("Unmatched opening brace")
         );
     BOOST_TEST_THROW
-        (test_interpolate("${x${y}}")
+        (test_interpolate("{{x{{y}}}}")
         ,std::runtime_error
         ,lmi_test::what_regex("Unexpected nested interpolation")
         );
+    BOOST_TEST_THROW
+        (section_test("{{#var1}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unclosed section 'var1'")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{^var0}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unclosed section 'var0'")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{/var1}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unexpected end of section")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{#var1}}{{/var0}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unexpected end of section")
+        );
 
     // Or because the lookup function throws.
     BOOST_TEST_THROW
         (interpolate_string
-            ("${x}"
+            ("{{x}}"
             ,[](std::string const& s) -> std::string
                 {
                 throw std::runtime_error("no such variable '" + s + "'");
@@ -65,6 +140,5 @@ int test_main(int, char*[])
         ,"no such variable 'x'"
         );
 
-
     return EXIT_SUCCESS;
 }
diff --git a/ledger_pdf_generator_wx.cpp b/ledger_pdf_generator_wx.cpp
index 2f4c0c3..8cad729 100644
--- a/ledger_pdf_generator_wx.cpp
+++ b/ledger_pdf_generator_wx.cpp
@@ -74,8 +74,11 @@ class html_interpolator
     // A method which can be used to interpolate an HTML string containing
     // references to the variables defined for this illustration. The general
     // syntax is the same as in the global interpolate_string() function, i.e.
-    // variables are anything of the form "${name}". The variable names
-    // understood by this function are:
+    // variables are of the form "{{name}}" and section of the form
+    // "{{#name}}..{{/name}}" or "{{^name}}..{{/name}}" are also allowed and
+    // their contents is included in the expansion if and only if the variable
+    // with the given name has value "1" for the former or "0" for the latter.
+    // The variable names understood by this function are:
     //  - Scalar fields of GetLedgerInvariant().
     //  - Special variables defined in this class, such as "lmi_version" and
     //    "date_prepared".
@@ -117,7 +120,7 @@ class html_interpolator
     // to false or true respectively. Anything else results in an exception.
     bool test_variable(std::string const& name) const
     {
-        auto const z = expand_simple_html(name).as_html();
+        auto const z = expand_html(name).as_html();
         return
               z == "1" ? true
             : z == "0" ? false
@@ -128,25 +131,9 @@ class html_interpolator
     }
 
   private:
-    // Highest level variable expansion function.
+    // The expansion function used with interpolate_string().
     text expand_html(std::string const& s) const
     {
-        // Check for the special "${var?only-if-set}" form:
-        auto const pos_question = s.find('?');
-        if(pos_question != std::string::npos)
-            {
-            return test_variable(s.substr(0, pos_question))
-                    ? text::from(s.substr(pos_question + 1))
-                    : text()
-                    ;
-            }
-
-        return expand_simple_html(s);
-    }
-
-    // Simple expansion for just the variable name.
-    text expand_simple_html(std::string const& s) const
-    {
         // Check our own variables first:
         auto const it = vars_.find(s);
         if(it != vars_.end())
@@ -397,7 +384,7 @@ class cover_page : public page
                 (tag::tr
                     (tag::td[attr::align("center")]
                         (tag::font[attr::size("+2")]
-                            (interpolate_html("${date_prepared}")
+                            (interpolate_html("{{date_prepared}}")
                             )
                         )
                     )
@@ -417,9 +404,9 @@ class cover_page : public page
             (tag::font[attr::size("-1")]
                 (interpolate_html
                     (R"(
-${InsCoShortName} Financial Group is a marketing
-name for ${InsCoName} (${InsCoShortName}) and its
-affiliated company and sales representatives, ${InsCoAddr}.
+{{InsCoShortName}} Financial Group is a marketing
+name for {{InsCoName}} ({{InsCoShortName}}) and its
+affiliated company and sales representatives, {{InsCoAddr}}.
 )"
                     )
                 )
@@ -479,13 +466,15 @@ class narrative_summary_page : public page
         if(!interpolate_html.test_variable("SinglePremium"))
             {
             description = R"(
-${PolicyMktgName} is a ${GroupExperienceRating?group}${GroupCarveout?group}
+{{PolicyMktgName}} is a
+{{#GroupExperienceRating}}group{{/GroupExperienceRating}}
+{{#GroupCarveout}}group{{/GroupCarveout}}
 flexible premium adjustable life insurance contract.
-${GroupExperienceRating?
+{{#GroupExperienceRating}}
 It is a no-load policy and is intended for large case sales.
 It is primarily marketed to financial institutions
 to fund certain corporate liabilities.
-}
+{{/GroupExperienceRating}}
 It features accumulating account values, adjustable benefits,
 and flexible premiums.
 )";
@@ -495,7 +484,7 @@ and flexible premiums.
                )
             {
             description = R"(
-${PolicyMktgName}
+{{PolicyMktgName}}
 is a modified single premium adjustable life
 insurance contract. It features accumulating
 account values, adjustable benefits, and single premium.
@@ -504,7 +493,7 @@ account values, adjustable benefits, and single premium.
         else
             {
             description = R"(
-${PolicyMktgName}
+{{PolicyMktgName}}
 is a single premium adjustable life insurance contract.
 It features accumulating account values,
 adjustable benefits, and single premium.
@@ -519,25 +508,25 @@ adjustable benefits, and single premium.
                 (R"(
 Coverage may be available on a Guaranteed Standard Issue basis.
 All proposals are based on case characteristics and must
-be approved by the ${InsCoShortName}
+be approved by the {{InsCoShortName}}
 Home Office. For details regarding underwriting
 and coverage limitations refer to your offer letter
-or contact your ${InsCoShortName} representative.
+or contact your {{InsCoShortName}} representative.
 )"
                 );
             }
 
         summary_html += add_body_paragraph_html
-            ( interpolate_html("${AvName}")
+            ( interpolate_html("{{AvName}}")
             + text::nbsp()
-            + interpolate_html("${MonthlyChargesPaymentFootnote}")
+            + interpolate_html("{{MonthlyChargesPaymentFootnote}}")
             );
 
         std::string premiums;
         if(!interpolate_html.test_variable("SinglePremium"))
             {
             premiums = R"(
-Premiums are assumed to be paid on ${ErModeLCWithArticle}
+Premiums are assumed to be paid on {{ErModeLCWithArticle}}
 basis and received at the beginning of the contract year.
 )";
             }
@@ -550,7 +539,7 @@ of the contract year.
             }
 
         premiums += R"(
-${AvName} Values, ${CsvName} Values,
+{{AvName}} Values, {{CsvName}} Values,
 and death benefits are illustrated as of the end
 of the contract year. The method we use to allocate
 overhead expenses is the fully allocated expense method.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]