diff --git a/src/lib/text-template.hpp b/src/lib/text-template.hpp
index d42208d49..f2ffc22bf 100644
--- a/src/lib/text-template.hpp
+++ b/src/lib/text-template.hpp
@@ -101,7 +101,8 @@
#include "lib/nocopy.hpp"
#include "lib/iter-index.hpp"
#include "lib/iter-explorer.hpp"
-#include "lib/format-util.hpp"///////////////////OOO use format-string??
+#include "lib/format-string.hpp"
+#include "lib/format-util.hpp"
#include "lib/regex.hpp"
#include "lib/util.hpp"
@@ -113,12 +114,14 @@
namespace lib {
+ namespace error = lumiera::error;
using std::optional;
using std::nullopt;
using std::string;
using StrView = std::string_view;
+ using util::_Fmt;
using util::unConst;
@@ -183,7 +186,7 @@ namespace lib {
if ("for" == mat[4])
tag.syntax = mat[3].matched? TagSyntax::END_FOR : TagSyntax::FOR;
else
- throw error::Logic("unexpected keyword");
+ throw error::Logic(_Fmt{"unexpected keyword \"%s\""} % mat[4]);
}
else
if (mat[2].matched)
@@ -334,11 +337,60 @@ namespace lib {
void
compile (PAR& parseIter, ActionSeq& actions)
{
- auto add = [&](Code c, string v){ actions.push_back (Action{c,v}); };
- auto addCode = [&](Code c) { add ( c, parseIter->key); };
- auto addLead = [&] { add (TEXT, string{parseIter->lead}); };
- auto openScope = [&](Clause c){ scope_.push (ParseCtx{c, actions.size()});};
+ auto currIDX = [&]{ return actions.size(); };
+ auto valid = [&](Idx i){ return 0 < i and i < actions.size(); };
+ auto clause = [](Clause c)-> string { return c==IF? "if" : "for"; };
+ auto scopeClause = [&]{ return scope_.empty()? "??" : clause(scope_.top().clause); };
+ // Support for bracketing constructs (if / for)
+ auto beginIdx = [&]{ return scope_.empty()? 0 : scope_.top().begin; }; // Index of action where scope was opened
+ auto scopeKey = [&]{ return valid(beginIdx())? actions[beginIdx()].val : "";}; // Key controlling the if-/for-Scope
+ auto keyMatch = [&]{ return isnil(parseIter->key) or parseIter->key == scopeKey(); }; // Key matches in opening and closing tag
+ auto clauseMatch = [&](Clause c){ return not scope_.empty() and scope_.top().clause == c; }; // Kind of closing tag matches innermost scope
+ auto scopeMatch = [&](Clause c){ return clauseMatch(c) and keyMatch(); };
+
+ auto lead = [&]{ return parseIter->lead; };
+ auto clashLead = [&]{ return actions[scope_.top().after - 1].val; }; // (for diagnostics: lead before a conflicting other "else")
+ auto abbrev = [&](auto s){ return s.length()<16? s : s.substr(s.length()-15); }; // (shorten lead display to 15 chars)
+
+ // Syntax / consistency checks...
+ auto __checkBalanced = [&](Clause c)
+ {
+ if (not scopeMatch(c))
+ throw error::Invalid{_Fmt{"Unbalanced Logic: expect ${end %s %s}"
+ " -- found ...%s${end |↯|%s %s}"}
+ % scopeClause() % scopeKey()
+ % abbrev(lead())
+ % clause(c) % parseIter->key
+ };
+ };
+ auto __checkInScope = [&] {
+ if (scope_.empty())
+ throw error::Invalid{_Fmt{"Misplaced ...%s|↯|${else}"}
+ % abbrev(lead())};
+ };
+ auto __checkNoDup = [&] {
+ if (scope_.top().after != 0)
+ throw error::Invalid{_Fmt{"Conflicting ...%s${else} ⟷ ...%s|↯|${else}"}
+ % abbrev(clashLead()) % abbrev(lead())};
+ };
+
+ // Primitives used for code generation....
+ auto add = [&](Code c, string v){ actions.push_back (Action{c,v});};
+ auto addCode = [&](Code c) { add ( c, parseIter->key); }; // add code token and transfer key picked up by parser
+ auto addLead = [&] { add (TEXT, string{parseIter->lead}); }; // add TEXT token to represent the static part before this tag
+ auto openScope = [&](Clause c){ scope_.push (ParseCtx{c, currIDX()}); }; // start nested scope for bracketing construct (if / for)
+ auto closeScope = [&] { scope_.pop(); }; // close innermost nested scope
+
+ auto linkElseToStart = [&]{ actions[beginIdx()].refIDX = currIDX(); }; // link the start position of the else-branch into opening logic code
+ auto markJumpInScope = [&]{ scope_.top().after = currIDX(); }; // memorise jump before else-branch for later linkage
+ auto linkLoopBack = [&]{ actions.back().refIDX = scope_.top().begin; }; // fill in the back-jump position at loop end
+ auto linkJumpToNext = [&]{ actions[scope_.top().after].refIDX = currIDX(); }; // link jump to the position after the end of the logic bracket
+
+ auto hasElse = [&]{ return scope_.top().after != 0; }; // a jump code to link was only marked if there was an else tag
+
+
+ /* === Code Generation === */
switch (parseIter->syntax) {
case TagSyntax::ESCAPE:
addLead();
@@ -354,31 +406,48 @@ namespace lib {
break;
case TagSyntax::END_IF:
addLead();
- ///////////////////////////////////////////////////OOO verify and pop IF-clause here
-// if (scope_.empty() or
-// (not isnil(tag.key) scope_.top())
+ __checkBalanced(IF);
+ if (hasElse())
+ linkJumpToNext();
+ else
+ linkElseToStart();
+ closeScope();
break;
case TagSyntax::FOR:
addLead();
openScope(FOR);
- ///////////////////////////////////////////////////OOO push FOR-clause here
addCode(ITER);
break;
case TagSyntax::END_FOR:
addLead();
- ///////////////////////////////////////////////////OOO verify and pop FOR-clause here
+ __checkBalanced(FOR);
+ if (hasElse())
+ linkJumpToNext();
+ else
+ { // no else-branch; end active loop here
+ addCode(LOOP);
+ linkLoopBack();
+ linkElseToStart(); // jump behind when iteration turns out empty
+ }
+ closeScope();
break;
case TagSyntax::ELSE:
addLead();
- if (true) /////////////////////////////////////////OOO derive IF or FOR from context
+ __checkInScope();
+ __checkNoDup();
+ if (IF == scope_.top().clause)
{
- ///////////////////////////////////////////////////OOO actual IF-else implementation
+ markJumpInScope();
addCode(JUMP);
+ linkElseToStart();
}
else
{
- ///////////////////////////////////////////////////OOO actual FOR-else implementation
addCode(LOOP);
+ linkLoopBack();
+ markJumpInScope();
+ addCode(JUMP);
+ linkElseToStart(); // jump to else-block when iteration turns out empty
}
break;
default:
diff --git a/tests/library/text-template-test.cpp b/tests/library/text-template-test.cpp
index 9faca41e2..0de2f8600 100644
--- a/tests/library/text-template-test.cpp
+++ b/tests/library/text-template-test.cpp
@@ -236,6 +236,92 @@ for} tail...
)~";
auto actions = TextTemplate::ActionCompiler().buildActions(parse(input));
SHOW_EXPR(util::join (explore(actions).transform(render),"▶\n▶"))
+ CHECK (25 == actions.size());
+
+ CHECK (actions[ 0].code == TextTemplate::Code::TEXT);
+ CHECK (actions[ 0].val == "\n Prefix-1 "_expect); // static text prefix
+ CHECK (actions[ 0].refIDX == 0);
+
+ CHECK (actions[ 1].code == TextTemplate::Code::KEY); // a placeholder to be substituted
+ CHECK (actions[ 1].val == "some.key"_expect); // use "some.key" for data retrieval
+
+ CHECK (actions[ 2].code == TextTemplate::Code::TEXT); // static text between active fields
+ CHECK (actions[ 2].val == " next one is "_expect);
+
+ CHECK (actions[ 3].code == TextTemplate::Code::TEXT); // since next tag was escaped, it appears in static segment
+ CHECK (actions[ 3].val == "\\${escaped}\n Prefix-2 "_expect);
+
+ CHECK (actions[ 4].code == TextTemplate::Code::COND); // start of an if-bracket construct
+ CHECK (actions[ 4].val == "cond1"_expect); // data marked with "cond1" will be used to determine true/false
+ CHECK (actions[ 4].refIDX == 7 ); // IDX ≡ 7 marks start of the else-branch
+
+ CHECK (actions[ 5].code == TextTemplate::Code::TEXT); // this static block will only be included if "cond1" evaluates to true
+ CHECK (actions[ 5].val == " active "_expect);
+
+ CHECK (actions[ 6].code == TextTemplate::Code::JUMP); // unconditional jump at the end of the if-true-block
+ CHECK (actions[ 6].val == ""_expect);
+ CHECK (actions[ 6].refIDX == 8 ); // IDX ≡ 8 points to the next element after the conditional construct
+
+ CHECK (actions[ 7].code == TextTemplate::Code::TEXT); // this static (else)-block will be included if "cond1" does not hold
+ CHECK (actions[ 7].val == " inactive "_expect);
+
+ CHECK (actions[ 8].code == TextTemplate::Code::TEXT); // again a static segment, displayed unconditionally
+ CHECK (actions[ 8].val == "Prefix-3 "_expect); // Note: no newline, since the closing bracket was placed at line start
+
+ CHECK (actions[ 9].code == TextTemplate::Code::COND); // again a conditional (but this time without else-branch)
+ CHECK (actions[ 9].val == "cond2"_expect); // data marked with "cond2" will be evaluated as condition
+ CHECK (actions[ 9].refIDX == 11 ); // IDX ≡ 11 is the alternative route, this time pointing behind the conditional
+
+ CHECK (actions[10].code == TextTemplate::Code::TEXT); // static text block to be displayed as content of the conditional
+ CHECK (actions[10].val == " active2"_expect);
+
+ CHECK (actions[11].code == TextTemplate::Code::TEXT); // again an unconditional static segment (behind end of preceding conditional)
+ CHECK (actions[11].val == " more\n Prefix-4 "_expect);
+
+ CHECK (actions[12].code == TextTemplate::Code::ITER); // Start of a for-construct (iteration)
+ CHECK (actions[12].val == "data"_expect); // data marked with "data" will be used to find and iterate nested elements
+ CHECK (actions[12].refIDX == 23 ); // IDX ≡ 23 points to the alternative "else" block, in case no iteration takes place
+
+ CHECK (actions[13].code == TextTemplate::Code::TEXT); // static block to appear for each nested "data" element
+ CHECK (actions[13].val == " fixed "_expect);
+
+ CHECK (actions[14].code == TextTemplate::Code::KEY); // placeholder to be substituted
+ CHECK (actions[14].val == "embedded"_expect); // _typically_ the data "embedded" will live in the iterated, nested elements
+
+ CHECK (actions[15].code == TextTemplate::Code::TEXT); // again a static block, which however lives within the iterated segment
+ CHECK (actions[15].val == "\n Pre-5 "_expect);
+
+ CHECK (actions[16].code == TextTemplate::Code::COND); // a nested conditional, thus nested on second level within the iteration construct
+ CHECK (actions[16].val == "nested"_expect); // data marked with "nested" will control the conditional (typically from iterated data elements)
+ CHECK (actions[16].refIDX == 19 ); // IDX ≡ 19 points to the else-block of this nested conditional
+
+ CHECK (actions[17].code == TextTemplate::Code::TEXT); // static content to appear as nested if-true-section
+ CHECK (actions[17].val == "nested-active"_expect);
+
+ CHECK (actions[18].code == TextTemplate::Code::JUMP); // jump code at end of the true-section
+ CHECK (actions[18].val == ""_expect);
+ CHECK (actions[18].refIDX == 20 ); // IDX ≡ 20 points behind the end of this nested conditional construct
+
+ CHECK (actions[19].code == TextTemplate::Code::TEXT); // static content comprising the else-section
+ CHECK (actions[19].val == "nested-inactive"_expect); // Note: no whitespace due to placement of the tag brackets of "else" / "end if"
+
+ CHECK (actions[20].code == TextTemplate::Code::TEXT); // again an unconditional static segment, yet still within the looping construct
+ CHECK (actions[20].val == "loop-suffix"_expect);
+
+ CHECK (actions[21].code == TextTemplate::Code::LOOP); // the loop-end code, where evaluation will consider the next iteration
+ CHECK (actions[21].val == ""_expect);
+ CHECK (actions[21].refIDX == 12 ); // IDX ≡ 12 points back to the opening ITER code
+
+ CHECK (actions[22].code == TextTemplate::Code::JUMP); // if however the iteration is complete, evaluation will jump over the "else" section
+ CHECK (actions[22].val == ""_expect);
+ CHECK (actions[22].refIDX == 24 );
+
+ CHECK (actions[23].code == TextTemplate::Code::TEXT); // this static else-segment will appear whenever no iteration takes place
+ CHECK (actions[23].val == ""_expect); // Note: in this example there is an ${else}-tag, yet the content is empty
+
+ CHECK (actions[24].code == TextTemplate::Code::TEXT); // a final static segment after the last active tag
+ CHECK (actions[24].val == " tail...\n"_expect);
+ CHECK (actions[24].refIDX == 0);
}
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm
index 34a00e2a1..0809d0e1b 100644
--- a/wiki/thinkPad.ichthyo.mm
+++ b/wiki/thinkPad.ichthyo.mm
@@ -112628,8 +112628,8 @@ std::cout << tmpl.render({"what", "World"}) << s
-
-
+
+
@@ -112656,16 +112656,16 @@ std::cout << tmpl.render({"what", "World"}) << s
-
-
-
-
-
+
+
+
+
+
-
+
-
-
+
+
@@ -112678,11 +112678,11 @@ std::cout << tmpl.render({"what", "World"}) << s
-
-
-
+
+
+
-
+
@@ -112706,16 +112706,16 @@ std::cout << tmpl.render({"what", "World"}) << s
-
-
+
+
-
-
+
+
@@ -112733,8 +112733,8 @@ std::cout << tmpl.render({"what", "World"}) << s
-
-
+
+
@@ -112758,16 +112758,16 @@ std::cout << tmpl.render({"what", "World"}) << s