163#ifndef LIB_TEXT_TEMPLATE_H
164#define LIB_TEXT_TEMPLATE_H
187 class TextTemplate_test;
191 using StrView = std::string_view;
198 namespace text_template {
201 const string MATCH_DATA_TOKEN = R
"~(([^,;"\s]*)\s*)~";
206 iterNestedKeys (
string key, StrView
const& iterDef)
209 .transform ([key](smatch mat){
return key+
"."+
string{mat[1]}+
"."; });
213 const string MATCH_BINDING_KEY = R
"~(([\w\.]+))~";
214 const string MATCH_BINDING_VAL = R
"~(([^,;"\s]+)\s*)~";
215 const string MATCH_QUOTED_VAL = R
"~("([^"]+)"\s*)~";
216 const string MATCH_BINDING_TOK = MATCH_BINDING_KEY+
"\\s*=\\s*(?:"+MATCH_BINDING_VAL+
"|"+MATCH_QUOTED_VAL+
")";
220 iterBindingSeq (
string const& dataDef)
223 .transform ([&](smatch mat){
return std::make_pair (
string{mat[1]}
224 ,
string{mat[3].matched? mat[3]:mat[2]}); });
229 const string MATCH_SINGLE_KEY =
"[A-Za-z_]+\\w*";
230 const string MATCH_KEY_PATH = MATCH_SINGLE_KEY+
"(?:\\."+MATCH_SINGLE_KEY+
")*";
231 const string MATCH_LOGIC_TOK =
"if|for";
232 const string MATCH_END_TOK =
"end\\s*";
233 const string MATCH_ELSE_TOK =
"else";
234 const string MATCH_SYNTAX =
"("+MATCH_ELSE_TOK+
")|(?:("+MATCH_END_TOK+
")?("+MATCH_LOGIC_TOK+
")\\s*)?("+MATCH_KEY_PATH+
")?";
235 const string MATCH_FIELD =
"\\$\\{\\s*(?:"+MATCH_SYNTAX+
")\\s*\\}";
236 const string MATCH_ESCAPE = R
"~((\\\$))~";
238 const regex ACCEPT_MARKUP { MATCH_ESCAPE+
"|"+MATCH_FIELD
239 , regex::ECMAScript|regex::optimize
253 Keyword syntax{ESCAPE};
260 parse (
string const& input)
262 auto classify = [rest=
StrView(input)]
263 (smatch mat)
mutable -> TagSyntax
265 REQUIRE (not mat.empty());
267 auto restAhead = mat.length() + mat.suffix().length();
268 auto pre = rest.length() - restAhead;
269 tag.lead = rest.substr(0, pre);
270 rest = rest.substr(tag.lead.length());
274 rest = rest.substr(1);
277 rest = rest.substr(mat.length());
281 tag.syntax = mat[3].matched? TagSyntax::END_IF : TagSyntax::IF;
284 tag.syntax = mat[3].matched? TagSyntax::END_FOR : TagSyntax::FOR;
286 throw error::Logic{_Fmt{
"unexpected keyword \"%s\""} % mat[4]};
290 tag.syntax = TagSyntax::ELSE;
293 throw error::Invalid{_Fmt{
"unqualified \"end\" without logic-keyword:"
294 " ...%s${end |↯|}"} % tag.lead};
296 tag.syntax = TagSyntax::KEYID;
303 .transform (classify);
311 template<
class DAT,
typename SEL=
void>
352 using ScopeStack = std::stack<ParseCtx, std::vector<ParseCtx>>;
361 auto instantiate (InstanceCore<SRC>&)
const;
365 using ActionSeq = std::vector<Action>;
368 class ActionCompiler;
374 using ActionIter = IndexIter<const ActionSeq>;
375 using DataCtxIter = SRC::Iter;
376 using NestedCtx = std::pair<DataCtxIter, SRC>;
377 using CtxStack = std::stack<NestedCtx, std::vector<NestedCtx>>;
378 using Value = SRC::Value;
381 ActionIter actionIter_;
386 InstanceCore (ActionSeq
const& actions, SRC);
388 bool checkPoint()
const;
392 Value instantiateNext();
393 Value reInstatiate (Idx =
Idx(-1));
394 Value getContent(
string key);
395 bool conditional (
string key);
396 bool openIteration (
string key);
405 TextTemplate(
string spec)
406 : actions_{compile (spec)}
411 submit (DAT
const& data)
const;
415 render (DAT
const& data)
const;
419 apply (
string spec, DAT
const& data);
424 static ActionSeq compile (
string const&);
425 friend class test::TextTemplate_test;
444 class TextTemplate::ActionCompiler
451 buildActions (PAR&& parseIter)
455 compile (parseIter, actions);
462 compile (PAR& parseIter, ActionSeq& actions)
464 auto currIDX = [&]{
return actions.size(); };
465 auto valid = [&](
Idx i){
return 0 < i and i < actions.size(); };
466 auto clause = [](Clause c)->
string {
return c==IF?
"if" :
"for"; };
467 auto scopeClause = [&]{
return scope_.empty()?
"??" : clause(scope_.top().clause); };
470 auto beginIdx = [&]{
return scope_.empty()? 0 : scope_.top().begin; };
471 auto scopeKey = [&]{
return valid(beginIdx())? actions[beginIdx()].val :
"";};
472 auto keyMatch = [&]{
return isnil(parseIter->key) or parseIter->key == scopeKey(); };
473 auto clauseMatch = [&](Clause c){
return not scope_.empty() and scope_.top().clause == c; };
474 auto scopeMatch = [&](Clause c){
return clauseMatch(c) and keyMatch(); };
476 auto lead = [&]{
return parseIter->lead; };
477 auto clashLead = [&]{
return actions[scope_.top().after - 1].val; };
478 auto abbrev = [&](
auto s){
return s.length()<16? s : s.substr(s.length()-15); };
481 auto __requireKey = [&](
string descr)
483 if (isnil (parseIter->key))
485 % abbrev(lead()) % descr
487 auto __checkBalanced = [&](Clause c)
489 if (not scopeMatch(c))
491 " -- found ...%s${end |↯|%s %s}"}
492 % scopeClause() % scopeKey()
494 % clause(c) % parseIter->key
496 auto __checkInScope = [&] {
501 auto __checkNoDup = [&] {
502 if (scope_.top().after != 0)
503 throw error::Invalid{_Fmt{
"Conflicting ...%s${else} ⟷ ...%s|↯|${else}"}
504 % abbrev(clashLead()) % abbrev(lead())};
506 auto __checkClosed = [&] {
507 if (not scope_.empty())
508 throw error::Invalid{_Fmt{
"Unclosed Logic tags: |↯|${end %s %s} missing"}
509 % scopeClause() % scopeKey()};
513 auto add = [&](Code c,
string v){ actions.push_back (Action{c,v});};
514 auto addCode = [&](Code c) { add ( c, parseIter->key); };
515 auto addLead = [&] { add (TEXT,
string{parseIter->lead}); };
516 auto openScope = [&](Clause c){ scope_.push (ParseCtx{c, currIDX()}); };
517 auto closeScope = [&] { scope_.pop(); };
519 auto linkElseToStart = [&]{ actions[beginIdx()].refIDX = currIDX(); };
520 auto markJumpInScope = [&]{ scope_.top().after = currIDX(); };
521 auto linkLoopBack = [&]{ actions.back().refIDX = scope_.top().begin; };
522 auto linkJumpToNext = [&]{ actions[scope_.top().after].refIDX = currIDX(); };
524 auto hasElse = [&]{
return scope_.top().after != 0; };
526 using text_template::TagSyntax;
529 switch (parseIter->syntax) {
530 case TagSyntax::ESCAPE:
533 case TagSyntax::KEYID:
534 __requireKey(
"<placeholder>");
539 __requireKey(
"if <conditional>");
544 case TagSyntax::END_IF:
554 __requireKey(
"for <data-id>");
559 case TagSyntax::END_FOR:
561 __checkBalanced(FOR);
572 case TagSyntax::ELSE:
576 if (IF == scope_.top().clause)
592 NOTREACHED (
"uncovered TagSyntax keyword while compiling a TextTemplate.");
595 StrView tail = parseIter->tail;
599 add (TEXT,
string{tail});
605 inline TextTemplate::ActionSeq
606 TextTemplate::compile (
string const& spec)
608 ActionSeq code = ActionCompiler().buildActions (text_template::parse (spec));
610 throw error::Invalid (
"TextTemplate spec without active placeholders.");
621 namespace text_template {
623 template<
class DAT,
typename SEL>
626 static_assert (not
sizeof(DAT),
627 "unable to bind this data source "
628 "for TextTemplate instantiation");
630 DataSource (DAT
const&);
633 using MapS = std::map<string,string>;
654 struct DataSource<
MapS>
656 MapS const * data_{
nullptr};
659 bool isSubScope() {
return not
isnil (keyPrefix_); }
661 DataSource() =
default;
662 DataSource(MapS
const& map)
667 using Value = std::string_view;
668 using Iter =
decltype(iterNestedKeys(
"",
""));
678 retrieveContent (
string key)
680 MapS::const_iterator elm;
683 elm = data_->find (keyPrefix_+key);
684 if (elm == data_->end())
685 elm = data_->find (key);
688 elm = data_->find (key);
689 ENSURE (elm != data_->end());
694 getSequence (
string key)
699 return iterNestedKeys (key, retrieveContent(key));
703 openContext (Iter&
iter)
706 DataSource nested{*
this};
707 nested.keyPrefix_ += *
iter;
713 using PairS = std::pair<string,string>;
717 struct DataSource<string>
720 std::shared_ptr<MapS> spec_;
722 DataSource (
string const& dataSpec)
726 explore (iterBindingSeq (dataSpec))
727 .foreach([
this](PairS
const& bind){ spec_->insert (bind); });
731 openContext (Iter&
iter)
733 DataSource nested(*
this);
734 auto nestedBase = DataSource<MapS>::openContext (
iter);
735 nested.keyPrefix_ = nestedBase.keyPrefix_;
744 template<
class STR,
typename = meta::enable_if<meta::is_StringLike<STR>> >
745 DataSource(STR
const&) -> DataSource<string>;
763 TextTemplate::Action::instantiate (InstanceCore<SRC>& core)
const
765 using Result =
decltype (core.getContent(val));
770 return core.getContent (val);
772 return core.conditional (val)? core.reInstatiate()
773 : core.reInstatiate(refIDX);
775 return core.reInstatiate(refIDX);
777 return core.openIteration(val)? core.reInstatiate()
778 : core.reInstatiate(refIDX);
780 return core.loopFurther() ? core.reInstatiate(refIDX+1)
781 : core.reInstatiate();
783 NOTREACHED (
"uncovered Activity verb in activation function.");
790 TextTemplate::InstanceCore<SRC>::InstanceCore (TextTemplate::ActionSeq
const& actions, SRC s)
792 , actionIter_{actions}
796 rendered_ = instantiateNext();
806 TextTemplate::InstanceCore<SRC>::checkPoint()
const
808 return bool(actionIter_);
813 TextTemplate::InstanceCore<SRC>::yield()
const
815 return unConst(
this)->rendered_;
820 TextTemplate::InstanceCore<SRC>::iterNext()
823 rendered_ = instantiateNext();
830 TextTemplate::InstanceCore<SRC>::instantiateNext()
832 return actionIter_? actionIter_->instantiate(*
this)
844 TextTemplate::InstanceCore<SRC>::reInstatiate (Idx nextCode)
846 if (nextCode ==
Idx(-1))
849 actionIter_.setIDX (nextCode);
850 return instantiateNext();
856 TextTemplate::InstanceCore<SRC>::getContent (
string key)
859 return dataSrc_.contains(key)? dataSrc_.retrieveContent(key) : nil;
865 TextTemplate::InstanceCore<SRC>::conditional (
string key)
867 return not
util::isNo (
string{getContent (key)});
883 TextTemplate::InstanceCore<SRC>::openIteration (
string key)
885 if (conditional (key))
886 if (DataCtxIter dataIter = dataSrc_.getSequence(key))
888 ctxStack_.push (NestedCtx{move (dataIter)
904 TextTemplate::InstanceCore<SRC>::loopFurther()
906 DataCtxIter& dataIter = ctxStack_.top().first;
916 swap (dataSrc_, ctxStack_.top().second);
934 TextTemplate::InstanceCore<SRC>::focusNested()
936 REQUIRE (not ctxStack_.empty());
937 NestedCtx& innermostScope = ctxStack_.top();
938 DataCtxIter& currentDataItem = innermostScope.first;
939 SRC& parentDataSrc = innermostScope.second;
941 this->dataSrc_ = parentDataSrc.openContext (currentDataItem);
955 TextTemplate::submit (DAT
const& data)
const
957 return explore (InstanceCore{actions_, text_template::DataSource(data)});
963 TextTemplate::render (DAT
const& data)
const
971 TextTemplate::apply (
string spec, DAT
const& data)
973 return TextTemplate(spec).render (data);
978 TextTemplate::keys()
const
981 .filter ([](Action
const& a){
return a.code == KEY or a.code == COND or a.code == ITER; })
982 .
transform([](Action
const& a){
return a.val; });
Derived specific exceptions within Lumiera's exception hierarchy.
Types marked with this mix-in may be moved but not copied.
A front-end for using printf-style formatting.
Lumiera error handling (C++ interface).
Iterator-style access handle to a referred container with subscript index.
Building tree expanding and backtracking evaluations within hierarchical scopes.
string render(DataCap const &)
_TransformIterT< IT, FUN >::Iter transform(IT &&source, FUN processingFunc)
pipes a given Lumiera Forward Iterator through a transformation function and wraps the resulting tran...
const string MATCH_DELIMITER
std::map< string, string > MapS
Implementation namespace for support and library code.
auto explore(IT &&srcSeq)
start building a IterExplorer by suitably wrapping the given iterable source.
Result(VAL &&) -> Result< VAL >
deduction guide: allow perfect forwarding of a any result into the ctor call.
LumieraError< LERR_(LOGIC)> Logic
LumieraError< LERR_(INVALID)> Invalid
Test runner and basic definitions for tests.
bool isNo(string const &textForm) noexcept
check if the given text is empty or can be interpreted as rejection (bool false)-
bool contains(MAP &map, typename MAP::key_type const &key)
shortcut for containment test on a map
OBJ * unConst(const OBJ *)
shortcut to save some typing when having to define const and non-const variants of member functions
string join(COLL &&coll, string const &delim=", ")
enumerate a collection's contents, separated by delimiter.
bool isnil(lib::time::Duration const &dur)
Mix-Ins to allow or prohibit various degrees of copying and cloning.
Convenience wrappers and helpers for dealing with regular expressions.
wrapped regex iterator to allow usage in foreach loops
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...