lmi-commits
[Top][All Lists]
Advanced

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

[lmi-commits] [lmi] master 0ecc8ec 036/156: Add support for partials to


From: Greg Chicares
Subject: [lmi-commits] [lmi] master 0ecc8ec 036/156: Add support for partials to our ad hoc Mustache parser
Date: Tue, 30 Jan 2018 17:22:04 -0500 (EST)

branch: master
commit 0ecc8ec7ff8542103b46e3022af643ea4b7ddeef
Author: Vadim Zeitlin <address@hidden>
Commit: Vadim Zeitlin <address@hidden>

    Add support for partials to our ad hoc Mustache parser
    
    Use recursion for partials expansion, this might not be the most
    efficient way to do it, but it is probably the simplest one.
    
    Do guard against overflowing the stack by arbitrarily limiting the
    recursion depth.
---
 interpolate_string.cpp      | 124 ++++++++++++++++++++++++++++++++------------
 interpolate_string.hpp      |   6 ++-
 interpolate_string_test.cpp |  65 +++++++++++++++++++++--
 3 files changed, 158 insertions(+), 37 deletions(-)

diff --git a/interpolate_string.cpp b/interpolate_string.cpp
index db3b9d3..f6153d1 100644
--- a/interpolate_string.cpp
+++ b/interpolate_string.cpp
@@ -29,45 +29,65 @@
 #include <stdexcept>
 #include <vector>
 
-std::string interpolate_string
+namespace
+{
+
+// Information about a single section 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.
+};
+
+// The only context we need is the stack of sections entered so far.
+using context = std::stack<section_info, std::vector<section_info>>;
+
+// The real interpolation recursive function, called by the public one to do
+// all the work.
+void do_interpolate_string_in_context
     (char const* s
     ,lookup_function const& lookup
+    ,std::string& out
+    ,context& sections
+    ,std::string const& partial = std::string()
+    ,int recursion_level = 0
     )
 {
-    std::string out;
-
-    // This is probably not going to be enough as replacements of the
-    // interpolated variables tend to be longer than the variables names
-    // themselves, but it's difficult to estimate the resulting string length
-    // 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)
+    // Guard against too deep recursion to avoid crashing on code using too
+    // many nested partials (either unintentionally, e.g. due to including a
+    // partial from itself, or maliciously).
+    //
+    // The maximum recursion level is chosen completely arbitrarily, the only
+    // criteria are that it shouldn't be too big to crash due to stack overflow
+    // before it is reached nor too small to break legitimate use cases.
+    if(recursion_level >= 100)
         {
+        alarum()
+            << "Nesting level too deep while expanding the partial \""
+            << partial
+            << "\""
+            << std::flush
+            ;
         }
 
-        // 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]()
@@ -171,6 +191,25 @@ std::string interpolate_string
                             sections.pop();
                             break;
 
+                        case '>':
+                            if(is_active())
+                                {
+                                auto const& real_name = name.substr(1);
+
+                                do_interpolate_string_in_context
+                                    (lookup
+                                        (real_name
+                                        ,interpolate_lookup_kind::partial
+                                        ).c_str()
+                                    ,lookup
+                                    ,out
+                                    ,sections
+                                    ,real_name
+                                    ,recursion_level + 1
+                                    );
+                                }
+                            break;
+
                         default:
                             if(is_active())
                                 {
@@ -217,6 +256,27 @@ std::string interpolate_string
             out += *p;
             }
         }
+}
+
+} // Unnamed namespace.
+
+std::string interpolate_string
+    (char const* s
+    ,lookup_function const& lookup
+    )
+{
+    std::string out;
+
+    // This is probably not going to be enough as replacements of the
+    // interpolated variables tend to be longer than the variables names
+    // themselves, but it's difficult to estimate the resulting string length
+    // any better than this.
+    out.reserve(strlen(s));
+
+    // The stack contains all the sections that we're currently in.
+    std::stack<section_info, std::vector<section_info>> sections;
+
+    do_interpolate_string_in_context(s, lookup, out, sections);
 
     if(!sections.empty())
         {
diff --git a/interpolate_string.hpp b/interpolate_string.hpp
index ef2de4d..e71d1ed 100644
--- a/interpolate_string.hpp
+++ b/interpolate_string.hpp
@@ -30,7 +30,8 @@
 enum class interpolate_lookup_kind
 {
     variable,
-    section
+    section,
+    partial
 };
 
 using lookup_function
@@ -45,12 +46,13 @@ using lookup_function
 ///  - Simple variable expansion for {{variable}}.
 ///  - Conditional expansion using {{#variable}}...{{/variable}}.
 ///  - Negated checks of the form {{^variable}}...{{/variable}}.
+///  - Partials support, i.e. {{>filename}}.
 ///
 /// 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.
+///  - Lambdas, 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
diff --git a/interpolate_string_test.cpp b/interpolate_string_test.cpp
index 459a297..766c20d 100644
--- a/interpolate_string_test.cpp
+++ b/interpolate_string_test.cpp
@@ -82,6 +82,61 @@ int test_main(int, char*[])
         ,"ae"
         );
 
+    // Partials.
+    auto const partial_test = [](char const* s)
+        {
+        return interpolate_string
+            (s
+            ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+                {
+                if(s == "header")       return "[header with {{var}}]";
+                if(s == "footer")       return "[footer with {{var}}]";
+                if(s == "nested")       return "[header with {{>footer}}]";
+                if(s == "recursive")    return "{{>recursive}}";
+                if(s == "sec" )         return "1" ;
+                if(s == "var" )         return "variable" ;
+
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            );
+        };
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{>header}}")
+        ,"[header with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{>header}}{{var}} in body{{>footer}}")
+        ,"[header with variable]variable in body[footer with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{#sec}}{{>header}}{{/sec}}")
+        ,"[header with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("only{{^sec}}{{>header}}{{/sec}}{{>footer}}")
+        ,"only[footer with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{>nested}}")
+        ,"[header with [footer with variable]]"
+        );
+
+    BOOST_TEST_THROW
+        (partial_test("{{>recursive}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Nesting level too deep")
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("no {{^sec}}{{>recursive}}{{/sec}} problem")
+        ,"no  problem"
+        );
+
     // Some special cases.
     BOOST_TEST_EQUAL
         (interpolate_string
@@ -101,8 +156,9 @@ int test_main(int, char*[])
     // Check that the kind of variable being expanded is correct.
     BOOST_TEST_EQUAL
         (interpolate_string
-            ("{{#section1}}{{^section0}}{{variable}}{{/section0}}{{/section1}}"
-            ,[](std::string const& s, interpolate_lookup_kind kind)
+            ("{{>test}}"
+             "{{#section1}}{{^section0}}{{variable}}{{/section0}}{{/section1}}"
+             ,[](std::string const& s, interpolate_lookup_kind kind) -> 
std::string
                 {
                 switch(kind)
                     {
@@ -112,12 +168,15 @@ int test_main(int, char*[])
                     case interpolate_lookup_kind::section:
                         // Get rid of the "section" prefix.
                         return s.substr(7);
+
+                    case interpolate_lookup_kind::partial:
+                        return s + " partial included\n";
                     }
 
                 throw std::runtime_error("invalid lookup kind");
                 }
             )
-        ,"value of variable"
+        ,"test partial included\nvalue of variable"
         );
 
     // Should throw if the input syntax is invalid.



reply via email to

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