Lumiera  0.pre.03
»edit your freedom«
text-template.hpp
Go to the documentation of this file.
1 /*
2  TEXT-TEMPLATE.hpp - minimalistic text substitution engine
3 
4  Copyright (C) Lumiera.org
5  2024, Hermann Vosseler <Ichthyostega@web.de>
6 
7  This program is free software; you can redistribute it and/or
8  modify it under the terms of the GNU General Public License as
9  published by the Free Software Foundation; either version 2 of
10  the License, or (at your option) any later version.
11 
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with this program; if not, write to the Free Software
19  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 
21 */
22 
23 
172 #ifndef LIB_TEXT_TEMPLATE_H
173 #define LIB_TEXT_TEMPLATE_H
174 
175 
176 #include "lib/error.hpp"
177 #include "lib/nocopy.hpp"
178 #include "lib/iter-index.hpp"
179 #include "lib/iter-explorer.hpp"
180 #include "lib/format-string.hpp"
181 #include "lib/format-util.hpp"
182 #include "lib/regex.hpp"
183 #include "lib/util.hpp"
184 
185 #include <memory>
186 #include <string>
187 #include <vector>
188 #include <stack>
189 #include <map>
190 
191 
192 namespace lib {
193  namespace error = lumiera::error;
194 
195  namespace test { // declared friend for test access
196  class TextTemplate_test;
197  }
198 
199  using std::string;
200  using StrView = std::string_view;
201 
202  using util::_Fmt;
203  using util::isnil;
204  using util::unConst;
205 
206 
207  namespace text_template {
208 
209  //-----------Syntax-for-iteration-control-in-map------
210  const string MATCH_DATA_TOKEN = R"~(([^,;"\s]*)\s*)~";
211  const string MATCH_DELIMITER = R"~((?:^|,|;)\s*)~" ;
212  const regex ACCEPT_DATA_ELM {MATCH_DELIMITER + MATCH_DATA_TOKEN};
213 
214  inline auto
215  iterNestedKeys (string key, StrView const& iterDef)
216  {
217  return explore (util::RegexSearchIter{iterDef, ACCEPT_DATA_ELM})
218  .transform ([key](smatch mat){ return key+"."+string{mat[1]}+"."; });
219  }
220 
221  //-----------Syntax-for-key-value-data-from-string------
222  const string MATCH_BINDING_KEY = R"~(([\w\.]+))~";
223  const string MATCH_BINDING_VAL = R"~(([^,;"\s]+)\s*)~";
224  const string MATCH_QUOTED_VAL = R"~("([^"]+)"\s*)~";
225  const string MATCH_BINDING_TOK = MATCH_BINDING_KEY+"\\s*=\\s*(?:"+MATCH_BINDING_VAL+"|"+MATCH_QUOTED_VAL+")";
226  const regex ACCEPT_BINDING_ELM {MATCH_DELIMITER + MATCH_BINDING_TOK};
227 
228  inline auto
229  iterBindingSeq (string const& dataDef)
230  {
231  return explore (util::RegexSearchIter{dataDef, ACCEPT_BINDING_ELM})
232  .transform ([&](smatch mat){ return std::make_pair (string{mat[1]}
233  ,string{mat[3].matched? mat[3]:mat[2]}); });
234  }
235 
236 
237  //-----------Syntax-for-TextTemplate-tags--------
238  const string MATCH_SINGLE_KEY = "[A-Za-z_]+\\w*";
239  const string MATCH_KEY_PATH = MATCH_SINGLE_KEY+"(?:\\."+MATCH_SINGLE_KEY+")*";
240  const string MATCH_LOGIC_TOK = "if|for";
241  const string MATCH_END_TOK = "end\\s*";
242  const string MATCH_ELSE_TOK = "else";
243  const string MATCH_SYNTAX = "("+MATCH_ELSE_TOK+")|(?:("+MATCH_END_TOK+")?("+MATCH_LOGIC_TOK+")\\s*)?("+MATCH_KEY_PATH+")?";
244  const string MATCH_FIELD = "\\$\\{\\s*(?:"+MATCH_SYNTAX+")\\s*\\}";
245  const string MATCH_ESCAPE = R"~((\\\$))~";
246 
247  const regex ACCEPT_MARKUP { MATCH_ESCAPE+"|"+MATCH_FIELD
248  , regex::ECMAScript|regex::optimize
249  };
250  // Sub-Matches: 1 = ESCAPE; 2 = ELSE; 3 = END; 4 = LOGIC; 5 = KEY;
251 
252  struct TagSyntax
253  {
254  enum Keyword{ ESCAPE
255  , KEYID
256  , IF
257  , END_IF
258  , FOR
259  , END_FOR
260  , ELSE
261  };
262  Keyword syntax{ESCAPE};
263  StrView lead;
264  StrView tail;
265  string key;
266  };
267 
268  inline auto
269  parse (string const& input)
270  {
271  auto classify = [rest=StrView(input)]
272  (smatch mat) mutable -> TagSyntax
273  {
274  REQUIRE (not mat.empty());
275  TagSyntax tag;
276  auto restAhead = mat.length() + mat.suffix().length();
277  auto pre = rest.length() - restAhead;
278  tag.lead = rest.substr(0, pre);
279  rest = rest.substr(tag.lead.length());
280  if (mat[5].matched)
281  tag.key = mat[5];
282  if (mat[1].matched)
283  rest = rest.substr(1); // strip escape
284  else
285  { // not escaped but indeed active field
286  rest = rest.substr(mat.length());
287  if (mat[4].matched)
288  { // detected a logic keyword...
289  if ("if" == mat[4])
290  tag.syntax = mat[3].matched? TagSyntax::END_IF : TagSyntax::IF;
291  else
292  if ("for" == mat[4])
293  tag.syntax = mat[3].matched? TagSyntax::END_FOR : TagSyntax::FOR;
294  else
295  throw error::Logic{_Fmt{"unexpected keyword \"%s\""} % mat[4]};
296  }
297  else
298  if (mat[2].matched)
299  tag.syntax = TagSyntax::ELSE;
300  else
301  if ("end" == mat[5])
302  throw error::Invalid{_Fmt{"unqualified \"end\" without logic-keyword:"
303  " ...%s${end |↯|}"} % tag.lead};
304  else
305  tag.syntax = TagSyntax::KEYID;
306  }
307  tag.tail = rest;
308  return tag;
309  };
310 
311  return explore (util::RegexSearchIter{input, ACCEPT_MARKUP})
312  .transform (classify);
313  }
314 
315 
320  template<class DAT, typename SEL=void>
321  class DataSource;
322 
323  }//(namespace) text_template
324 
325 
326 
327 
328 
329 
330  /*************************************************/
341  {
342  enum Clause {
343  IF, FOR
344  };
345  enum Code {
346  TEXT, KEY, COND, JUMP, ITER, LOOP
347  };
348 
350  using Idx = size_t;
351 
352  template<class SRC>
354 
355  struct ParseCtx
356  {
357  Clause clause;
358  Idx begin{0};
359  Idx after{0};
360  };
361  using ScopeStack = std::stack<ParseCtx, std::vector<ParseCtx>>;
362 
363  struct Action
364  {
365  Code code{TEXT};
366  string val{};
367  Idx refIDX{0};
368 
369  template<class SRC>
370  auto instantiate (InstanceCore<SRC>&) const;
371  };
372 
374  using ActionSeq = std::vector<Action>;
375 
377  class ActionCompiler;
378 
380  template<class SRC>
381  class InstanceCore
382  {
383  using ActionIter = IterIndex<const ActionSeq>;
384  using DataCtxIter = typename SRC::Iter;
385  using NestedCtx = std::pair<DataCtxIter, SRC>;
386  using CtxStack = std::stack<NestedCtx, std::vector<NestedCtx>>;
387  using Value = typename SRC::Value;
388 
389  SRC dataSrc_;
390  ActionIter actionIter_;
391  CtxStack ctxStack_;
392  Value rendered_;
393 
394  public:
395  InstanceCore (ActionSeq const& actions, SRC);
396 
397  bool checkPoint() const;
398  auto& yield() const;
399  void iterNext();
400 
401  Value instantiateNext();
402  Value reInstatiate (Idx =Idx(-1));
403  Value getContent(string key);
404  bool conditional (string key);
405  bool openIteration (string key);
406  bool loopFurther();
407  void focusNested();
408  };
409 
410 
411  ActionSeq actions_;
412 
413  public:
414  TextTemplate(string spec)
415  : actions_{compile (spec)}
416  { }
417 
418  template<class DAT>
419  auto
420  submit (DAT const& data) const;
421 
422  template<class DAT>
423  string
424  render (DAT const& data) const;
425 
426  template<class DAT>
427  static string
428  apply (string spec, DAT const& data);
429 
430  auto keys() const;
431 
433  static ActionSeq compile (string const&);
434  friend class test::TextTemplate_test;
435  };
436 
437 
438 
439 
440  /* ======= Parser / Compiler pipeline ======= */
441 
454  {
455  ScopeStack scope_{};
456 
457  public:
458  template<class PAR>
459  ActionSeq
460  buildActions (PAR&& parseIter)
461  {
462  ActionSeq actions;
463  while (parseIter)
464  compile (parseIter, actions);
465  return actions;
466  }
467 
468  private:
469  template<class PAR>
470  void
471  compile (PAR& parseIter, ActionSeq& actions)
472  {
473  auto currIDX = [&]{ return actions.size(); };
474  auto valid = [&](Idx i){ return 0 < i and i < actions.size(); };
475  auto clause = [](Clause c)-> string { return c==IF? "if" : "for"; };
476  auto scopeClause = [&]{ return scope_.empty()? "??" : clause(scope_.top().clause); };
477 
478  // Support for bracketing constructs (if / for)
479  auto beginIdx = [&]{ return scope_.empty()? 0 : scope_.top().begin; }; // Index of action where scope was opened
480  auto scopeKey = [&]{ return valid(beginIdx())? actions[beginIdx()].val : "";}; // Key controlling the if-/for-Scope
481  auto keyMatch = [&]{ return isnil(parseIter->key) or parseIter->key == scopeKey(); }; // Key matches in opening and closing tag
482  auto clauseMatch = [&](Clause c){ return not scope_.empty() and scope_.top().clause == c; }; // Kind of closing tag matches innermost scope
483  auto scopeMatch = [&](Clause c){ return clauseMatch(c) and keyMatch(); };
484 
485  auto lead = [&]{ return parseIter->lead; };
486  auto clashLead = [&]{ return actions[scope_.top().after - 1].val; }; // (for diagnostics: lead before a conflicting other "else")
487  auto abbrev = [&](auto s){ return s.length()<16? s : s.substr(s.length()-15); }; // (shorten lead display to 15 chars)
488 
489  // Syntax / consistency checks...
490  auto __requireKey = [&](string descr)
491  {
492  if (isnil (parseIter->key))
493  throw error::Invalid{_Fmt{"Tag without key: ...%s${%s |↯|}"}
494  % abbrev(lead()) % descr
495  }; };
496  auto __checkBalanced = [&](Clause c)
497  {
498  if (not scopeMatch(c))
499  throw error::Invalid{_Fmt{"Unbalanced Logic: expect ${end %s %s}"
500  " -- found ...%s${end |↯|%s %s}"}
501  % scopeClause() % scopeKey()
502  % abbrev(lead())
503  % clause(c) % parseIter->key
504  }; };
505  auto __checkInScope = [&] {
506  if (scope_.empty())
507  throw error::Invalid{_Fmt{"Misplaced ...%s|↯|${else}"}
508  % abbrev(lead())};
509  };
510  auto __checkNoDup = [&] {
511  if (scope_.top().after != 0)
512  throw error::Invalid{_Fmt{"Conflicting ...%s${else} ⟷ ...%s|↯|${else}"}
513  % abbrev(clashLead()) % abbrev(lead())};
514  };
515  auto __checkClosed = [&] {
516  if (not scope_.empty())
517  throw error::Invalid{_Fmt{"Unclosed Logic tags: |↯|${end %s %s} missing"}
518  % scopeClause() % scopeKey()};
519  };
520 
521  // Primitives used for code generation....
522  auto add = [&](Code c, string v){ actions.push_back (Action{c,v});};
523  auto addCode = [&](Code c) { add ( c, parseIter->key); }; // add code token and transfer key picked up by parser
524  auto addLead = [&] { add (TEXT, string{parseIter->lead}); }; // add TEXT token to represent the static part before this tag
525  auto openScope = [&](Clause c){ scope_.push (ParseCtx{c, currIDX()}); }; // start nested scope for bracketing construct (if / for)
526  auto closeScope = [&] { scope_.pop(); }; // close innermost nested scope
527 
528  auto linkElseToStart = [&]{ actions[beginIdx()].refIDX = currIDX(); }; // link the start position of the else-branch into opening logic code
529  auto markJumpInScope = [&]{ scope_.top().after = currIDX(); }; // memorise jump before else-branch for later linkage
530  auto linkLoopBack = [&]{ actions.back().refIDX = scope_.top().begin; }; // fill in the back-jump position at loop end
531  auto linkJumpToNext = [&]{ actions[scope_.top().after].refIDX = currIDX(); }; // link jump to the position after the end of the logic bracket
532 
533  auto hasElse = [&]{ return scope_.top().after != 0; }; // a jump code to link was only marked if there was an else tag
534 
535  using text_template::TagSyntax;
536 
537  /* === Code Generation === */
538  switch (parseIter->syntax) {
539  case TagSyntax::ESCAPE:
540  addLead();
541  break;
542  case TagSyntax::KEYID:
543  __requireKey("<placeholder>");
544  addLead();
545  addCode(KEY);
546  break;
547  case TagSyntax::IF:
548  __requireKey("if <conditional>");
549  addLead();
550  openScope(IF);
551  addCode(COND);
552  break;
553  case TagSyntax::END_IF:
554  addLead();
555  __checkBalanced(IF);
556  if (hasElse())
557  linkJumpToNext();
558  else
559  linkElseToStart();
560  closeScope();
561  break;
562  case TagSyntax::FOR:
563  __requireKey("for <data-id>");
564  addLead();
565  openScope(FOR);
566  addCode(ITER);
567  break;
568  case TagSyntax::END_FOR:
569  addLead();
570  __checkBalanced(FOR);
571  if (hasElse())
572  linkJumpToNext();
573  else
574  { // no else-branch; end active loop here
575  addCode(LOOP);
576  linkLoopBack();
577  linkElseToStart(); // jump behind when iteration turns out empty
578  }
579  closeScope();
580  break;
581  case TagSyntax::ELSE:
582  addLead();
583  __checkInScope();
584  __checkNoDup();
585  if (IF == scope_.top().clause)
586  {
587  markJumpInScope();
588  addCode(JUMP);
589  linkElseToStart();
590  }
591  else
592  {
593  addCode(LOOP);
594  linkLoopBack();
595  markJumpInScope();
596  addCode(JUMP);
597  linkElseToStart(); // jump to else-block when iteration turns out empty
598  }
599  break;
600  default:
601  NOTREACHED ("uncovered TagSyntax keyword while compiling a TextTemplate.");
602  }
603 
604  StrView tail = parseIter->tail;
605  ++parseIter;
606  if (not parseIter)
607  {//add final action to supply text after last active tag
608  add (TEXT, string{tail});
609  __checkClosed();
610  }
611  }
612  };
613 
615  TextTemplate::compile (string const& spec)
616  {
617  ActionSeq code = ActionCompiler().buildActions (text_template::parse (spec));
618  if (isnil (code))
619  throw error::Invalid ("TextTemplate spec without active placeholders.");
620  return code;
621  }
622 
623 
624 
625 
626 
627 
628  /* ======= preconfigured data bindings ======= */
629 
630  namespace text_template {
631 
632  template<class DAT, typename SEL>
633  struct DataSource
634  {
635  static_assert (not sizeof(DAT),
636  "unable to bind this data source "
637  "for TextTemplate instantiation");
638 
639  DataSource (DAT const&);
640  };
641 
642  using MapS = std::map<string,string>;
643 
662  template<>
663  struct DataSource<MapS>
664  {
665  MapS const * data_{nullptr};
666  string keyPrefix_{};
667 
668  bool isSubScope() { return not isnil (keyPrefix_); }
669 
670  DataSource() = default;
671  DataSource(MapS const& map)
672  : data_{&map}
673  { }
674 
675 
676  using Value = std::string_view;
677  using Iter = decltype(iterNestedKeys("",""));
678 
679  bool
680  contains (string key)
681  {
682  return (isSubScope() and util::contains (*data_, keyPrefix_+key))
683  or util::contains (*data_, key);
684  }
685 
686  Value
687  retrieveContent (string key)
688  {
689  MapS::const_iterator elm;
690  if (isSubScope())
691  {
692  elm = data_->find (keyPrefix_+key);
693  if (elm == data_->end())
694  elm = data_->find (key);
695  }
696  else
697  elm = data_->find (key);
698  ENSURE (elm != data_->end());
699  return elm->second;
700  }
701 
702  Iter
703  getSequence (string key)
704  {
705  if (not contains(key))
706  return Iter{};
707  else
708  return iterNestedKeys (key, retrieveContent(key));
709  }
710 
711  DataSource
712  openContext (Iter& iter)
713  {
714  REQUIRE (iter);
715  DataSource nested{*this};
716  nested.keyPrefix_ += *iter;
717  return nested;
718  }
719  };
720 
721 
722  using PairS = std::pair<string,string>;
723 
725  template<>
726  struct DataSource<string>
727  : DataSource<MapS>
728  {
729  std::shared_ptr<MapS> spec_;
730 
731  DataSource (string const& dataSpec)
732  : spec_{new MapS}
733  {
734  data_ = spec_.get();
735  explore (iterBindingSeq (dataSpec))
736  .foreach([this](PairS const& bind){ spec_->insert (bind); });
737  }
738 
739  DataSource
740  openContext (Iter& iter)
741  {
742  DataSource nested(*this);
743  auto nestedBase = DataSource<MapS>::openContext (iter);
744  nested.keyPrefix_ = nestedBase.keyPrefix_;
745  return nested;
746  }
747  };
748 
753  template<class STR, typename = meta::enable_if<meta::is_StringLike<STR>> >
754  DataSource(STR const&) -> DataSource<string>;
755 
756  }// namespace text_template
757 
758 
759 
760 
761 
762  /* ======= implementation of the instantiation state ======= */
763 
770  template<class SRC>
771  inline auto
773  {
774  using Result = decltype (core.getContent(val));
775  switch (code) {
776  case TEXT:
777  return Result(val);
778  case KEY:
779  return core.getContent (val);
780  case COND:
781  return core.conditional (val)? core.reInstatiate() // next is the conditional content
782  : core.reInstatiate(refIDX); // points to start of else-block (or after)
783  case JUMP:
784  return core.reInstatiate(refIDX);
785  case ITER:
786  return core.openIteration(val)? core.reInstatiate() // looping initiated => continue with next
787  : core.reInstatiate(refIDX); // points to start of else-block (or after)
788  case LOOP:
789  return core.loopFurther() ? core.reInstatiate(refIDX+1) // start with one after the loop opening
790  : core.reInstatiate(); // continue with next -> jump over else-block
791  default:
792  NOTREACHED ("uncovered Activity verb in activation function.");
793  }
794  }
795 
796 
797 
798  template<class SRC>
800  : dataSrc_{s}
801  , actionIter_{actions}
802  , ctxStack_{}
803  , rendered_{}
804  {
805  rendered_ = instantiateNext();
806  }
807 
813  template<class SRC>
814  inline bool
816  {
817  return bool(actionIter_);
818  }
819 
820  template<class SRC>
821  inline auto&
823  {
824  return unConst(this)->rendered_;
825  }
826 
827  template<class SRC>
828  inline void
830  {
831  ++actionIter_;
832  rendered_ = instantiateNext();
833  }
834 
835 
837  template<class SRC>
838  inline typename SRC::Value
840  {
841  return actionIter_? actionIter_->instantiate(*this)
842  : Value{};
843  }
844 
851  template<class SRC>
852  inline typename SRC::Value
854  {
855  if (nextCode == Idx(-1))
856  ++actionIter_;
857  else
858  actionIter_.setIDX (nextCode);
859  return instantiateNext();
860  }
861 
863  template<class SRC>
864  inline typename SRC::Value
866  {
867  static Value nil{};
868  return dataSrc_.contains(key)? dataSrc_.retrieveContent(key) : nil;
869  }
870 
872  template<class SRC>
873  inline bool
875  {
876  return not util::isNo (string{getContent (key)});
877  }
878 
890  template<class SRC>
891  inline bool
893  {
894  if (conditional (key))
895  if (DataCtxIter dataIter = dataSrc_.getSequence(key))
896  {
897  ctxStack_.push (NestedCtx{move (dataIter)
898  ,dataSrc_});
899  focusNested();
900  return true;
901  }
902  return false;
903  }
904 
911  template<class SRC>
912  inline bool
914  {
915  DataCtxIter& dataIter = ctxStack_.top().first;
916  ++dataIter;
917  if (dataIter)
918  { // open next nested context *from enclosing context*
919  focusNested();
920  return true;
921  }
922  else
923  { // restore original data context
924  std::swap (dataSrc_, ctxStack_.top().second);
925  ctxStack_.pop();
926  return false;
927  }
928  }
929 
940  template<class SRC>
941  inline void
943  {
944  REQUIRE (not ctxStack_.empty());
945  NestedCtx& innermostScope = ctxStack_.top();
946  DataCtxIter& currentDataItem = innermostScope.first;
947  SRC& parentDataSrc = innermostScope.second;
948 
949  this->dataSrc_ = parentDataSrc.openContext (currentDataItem);
950  }
951 
952 
953 
954 
955 
961  template<class DAT>
962  inline auto
963  TextTemplate::submit (DAT const& data) const
964  {
965  return explore (InstanceCore{actions_, text_template::DataSource(data)});
966  }
967 
969  template<class DAT>
970  inline string
971  TextTemplate::render (DAT const& data) const
972  {
973  return util::join (submit (data), "");
974  }
975 
977  template<class DAT>
978  inline string
979  TextTemplate::apply (string spec, DAT const& data)
980  {
981  return TextTemplate(spec).render (data);
982  }
983 
985  inline auto
987  {
988  return explore (actions_)
989  .filter ([](Action const& a){ return a.code == KEY or a.code == COND or a.code == ITER; })
990  .transform([](Action const& a){ return a.val; });
991  }
992 
993 
994 }// namespace lib
995 #endif /*LIB_TEXT_TEMPLATE_H*/
Binding to a specific data source.
bool checkPoint() const
TextTemplate instantiation: check point on rendered Action.
Result(VAL &&) -> Result< VAL >
deduction guide: allow perfect forwarding of a any result into the ctor call.
auto explore(IT &&srcSeq)
start building a IterExplorer by suitably wrapping the given iterable source.
Definition: run.hpp:49
Adapter for the Map-Data-Source to consume a string spec (for testing)
bool conditional(string key)
retrieve a data value for the key and interpret it as boolean expression
Iterator-style access handle to a referred container with subscript index.
Types marked with this mix-in may be moved but not copied.
Definition: nocopy.hpp:58
Front-end for printf-style string template interpolation.
bool openIteration(string key)
Attempt to open data sequence by evaluating the entrance key.
Text template substitution engine.
auto submit(DAT const &data) const
Instantiate this (pre-compiled) TextTemplate using the given data binding.
A front-end for using printf-style formatting.
Implementation namespace for support and library code.
Iterator »State Core« to process the template instantiation.
Derived specific exceptions within Lumiera&#39;s exception hierarchy.
Definition: error.hpp:199
bool loopFurther()
Possibly continue the iteration within an already established nested scope.
Mix-Ins to allow or prohibit various degrees of copying and cloning.
string render(DAT const &data) const
submit data and materialise rendered results into a single string
Value instantiateNext()
Instantiate next Action token and expose its rendering.
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...
represents text content
auto instantiate(InstanceCore< SRC > &) const
Interpret an action token from the compiled text template based on the given data binding and iterati...
Value reInstatiate(Idx=Idx(-1))
relocate to another Action token and continue instantiation there
static ActionSeq compile(string const &)
auto keys() const
diagnostics: query a list of all active keys expected by the template.
Lumiera error handling (C++ interface).
wrapped regex iterator to allow usage in foreach loops
Definition: regex.hpp:49
static string apply(string spec, DAT const &data)
one-shot shorthand: compile a template and apply it to the given data
const string MATCH_DATA_TOKEN
< Parser and DataSource binding for lib::TextTemplate
std::vector< Action > ActionSeq
the text template is compiled into a sequence of Actions
Collection of small helpers and convenience shortcuts for diagnostics & formatting.
void focusNested()
Step down into the innermost data item context, prepared at the top of #ctxStack_.
Convenience wrappers and helpers for dealing with regular expressions.
Value getContent(string key)
retrieve a data value from the data source for the indiated key
Building tree expanding and backtracking evaluations within hierarchical scopes.
size_t Idx
cross-references by index number