[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 = [§ions]()
+ {
+ 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.
- [lmi-commits] [lmi] master efc01fa 046/156: Allow disabling separator lines in wx_table_generator, (continued)
- [lmi-commits] [lmi] master efc01fa 046/156: Allow disabling separator lines in wx_table_generator, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master e20544f 018/156: Add check for the ledger type, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master a0a167e 144/156: Rename "compliance_tracking_number" template to "imprimatur", Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 982c9f0 149/156: Remove consecutive blank lines from a Mustache template, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master ec73905 058/156: Add extra pair of braces to std::array<> initializer for clang, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 6a5cd32 079/156: Add "Table Rating" to the header if necessary, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master b01d478 029/156: Resurrect ledger XML IO code as new ledger_evaluator, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 89c676d 009/156: Add pdf_writer_wx::get_page_height() helper, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 8e26a76 004/156: Improve encapsulation by returning only wxDC from pdf_writer_wx, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master cb5cb7b 021/156: Add more contents to the narrative summary page, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 173cc28 022/156: Change interpolated strings syntax to be Mustache-like,
Greg Chicares <=
- [lmi-commits] [lmi] master 2e4850c 023/156: Implement the rest of "Narrative Summary" page body text, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master c341dbb 108/156: Factor out compliance_tracking template from the footer one, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 76881d8 019/156: Refactor: extra add_body_paragraph() helper, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 08127f1 028/156: Add symbolic constant for the "valign" HTML attribute, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 0d6c7f0 062/156: Get rid of separate wxDC parameter in the code, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master bbada54 084/156: Add image to the columns headings page too, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 3cf5a1c 056/156: Add wx_table_generator::output_super_header(), Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 95e616e 105/156: Add wx_table_generator::columns_count() accessor, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 3f1d437 088/156: Add add_abbreviated_variable() helper, Greg Chicares, 2018/01/30
- [lmi-commits] [lmi] master 118a249 097/156: Add "Column Definitions and Key Terms" page to NASD illustrations, Greg Chicares, 2018/01/30