Lumiera 0.pre.04
»edit your freedom«
Loading...
Searching...
No Matches
text-template.hpp
Go to the documentation of this file.
1/*
2 TEXT-TEMPLATE.hpp - minimalistic text substitution engine
3
4 Copyright (C)
5 2024, Hermann Vosseler <Ichthyostega@web.de>
6
7  **Lumiera** is free software; you can redistribute it and/or modify it
8  under the terms of the GNU General Public License as published by the
9  Free Software Foundation; either version 2 of the License, or (at your
10  option) any later version. See the file COPYING for further details.
11
12*/
13
14
163#ifndef LIB_TEXT_TEMPLATE_H
164#define LIB_TEXT_TEMPLATE_H
165
166
167#include "lib/error.hpp"
168#include "lib/nocopy.hpp"
169#include "lib/index-iter.hpp"
170#include "lib/iter-explorer.hpp"
171#include "lib/format-string.hpp"
172#include "lib/format-util.hpp"
173#include "lib/regex.hpp"
174#include "lib/util.hpp"
175
176#include <memory>
177#include <string>
178#include <vector>
179#include <stack>
180#include <map>
181
182
183namespace lib {
184 namespace error = lumiera::error;
185
186 namespace test { // declared friend for test access
187 class TextTemplate_test;
188 }
189
190 using std::string;
191 using StrView = std::string_view;
192
193 using util::_Fmt;
194 using util::isnil;
195 using util::unConst;
196
197
198 namespace text_template {
199
200 //-----------Syntax-for-iteration-control-in-map------
201 const string MATCH_DATA_TOKEN = R"~(([^,;"\s]*)\s*)~";
202 const string MATCH_DELIMITER = R"~((?:^|,|;)\s*)~" ;
203 const regex ACCEPT_DATA_ELM {MATCH_DELIMITER + MATCH_DATA_TOKEN};
204
205 inline auto
206 iterNestedKeys (string key, StrView const& iterDef)
207 {
208 return explore (util::RegexSearchIter{iterDef, ACCEPT_DATA_ELM})
209 .transform ([key](smatch mat){ return key+"."+string{mat[1]}+"."; });
210 }
211
212 //-----------Syntax-for-key-value-data-from-string------
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+")";
217 const regex ACCEPT_BINDING_ELM {MATCH_DELIMITER + MATCH_BINDING_TOK};
218
219 inline auto
220 iterBindingSeq (string const& dataDef)
221 {
222 return explore (util::RegexSearchIter{dataDef, ACCEPT_BINDING_ELM})
223 .transform ([&](smatch mat){ return std::make_pair (string{mat[1]}
224 ,string{mat[3].matched? mat[3]:mat[2]}); });
225 }
226
227
228 //-----------Syntax-for-TextTemplate-tags--------
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"~((\\\$))~";
237
238 const regex ACCEPT_MARKUP { MATCH_ESCAPE+"|"+MATCH_FIELD
239 , regex::ECMAScript|regex::optimize
240 };
241 // Sub-Matches: 1 = ESCAPE; 2 = ELSE; 3 = END; 4 = LOGIC; 5 = KEY;
242
243 struct TagSyntax
244 {
245 enum Keyword{ ESCAPE
246 , KEYID
247 , IF
248 , END_IF
249 , FOR
250 , END_FOR
251 , ELSE
252 };
253 Keyword syntax{ESCAPE};
254 StrView lead;
255 StrView tail;
256 string key;
257 };
258
259 inline auto
260 parse (string const& input)
261 {
262 auto classify = [rest=StrView(input)]
263 (smatch mat) mutable -> TagSyntax
264 {
265 REQUIRE (not mat.empty());
266 TagSyntax tag;
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());
271 if (mat[5].matched)
272 tag.key = mat[5];
273 if (mat[1].matched)
274 rest = rest.substr(1); // strip escape
275 else
276 { // not escaped but indeed active field
277 rest = rest.substr(mat.length());
278 if (mat[4].matched)
279 { // detected a logic keyword...
280 if ("if" == mat[4])
281 tag.syntax = mat[3].matched? TagSyntax::END_IF : TagSyntax::IF;
282 else
283 if ("for" == mat[4])
284 tag.syntax = mat[3].matched? TagSyntax::END_FOR : TagSyntax::FOR;
285 else
286 throw error::Logic{_Fmt{"unexpected keyword \"%s\""} % mat[4]};
287 }
288 else
289 if (mat[2].matched)
290 tag.syntax = TagSyntax::ELSE;
291 else
292 if ("end" == mat[5])
293 throw error::Invalid{_Fmt{"unqualified \"end\" without logic-keyword:"
294 " ...%s${end |↯|}"} % tag.lead};
295 else
296 tag.syntax = TagSyntax::KEYID;
297 }
298 tag.tail = rest;
299 return tag;
300 };
301
302 return explore (util::RegexSearchIter{input, ACCEPT_MARKUP})
303 .transform (classify);
304 }
305
306
311 template<class DAT, typename SEL=void>
312 class DataSource;
313
314 }//(namespace) text_template
315
316
317
318
319
320
321 /*************************************************/
330 class TextTemplate
332 {
333 enum Clause {
334 IF, FOR
335 };
336 enum Code {
337 TEXT, KEY, COND, JUMP, ITER, LOOP
338 };
339
341 using Idx = size_t;
342
343 template<class SRC>
344 class InstanceCore;
345
346 struct ParseCtx
347 {
348 Clause clause;
349 Idx begin{0};
350 Idx after{0};
351 };
352 using ScopeStack = std::stack<ParseCtx, std::vector<ParseCtx>>;
353
354 struct Action
355 {
356 Code code{TEXT};
357 string val{};
358 Idx refIDX{0};
359
360 template<class SRC>
361 auto instantiate (InstanceCore<SRC>&) const;
362 };
363
365 using ActionSeq = std::vector<Action>;
366
368 class ActionCompiler;
369
371 template<class SRC>
372 class InstanceCore
373 {
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;
379
380 SRC dataSrc_;
381 ActionIter actionIter_;
382 CtxStack ctxStack_;
383 Value rendered_;
384
385 public:
386 InstanceCore (ActionSeq const& actions, SRC);
387
388 bool checkPoint() const;
389 auto& yield() const;
390 void iterNext();
391
392 Value instantiateNext();
393 Value reInstatiate (Idx =Idx(-1));
394 Value getContent(string key);
395 bool conditional (string key);
396 bool openIteration (string key);
397 bool loopFurther();
398 void focusNested();
399 };
400
401
402 ActionSeq actions_;
403
404 public:
405 TextTemplate(string spec)
406 : actions_{compile (spec)}
407 { }
408
409 template<class DAT>
410 auto
411 submit (DAT const& data) const;
412
413 template<class DAT>
414 string
415 render (DAT const& data) const;
416
417 template<class DAT>
418 static string
419 apply (string spec, DAT const& data);
420
421 auto keys() const;
422
424 static ActionSeq compile (string const&);
425 friend class test::TextTemplate_test;
426 };
427
428
429
430
431 /* ======= Parser / Compiler pipeline ======= */
432
444 class TextTemplate::ActionCompiler
445 {
446 ScopeStack scope_{};
447
448 public:
449 template<class PAR>
450 ActionSeq
451 buildActions (PAR&& parseIter)
452 {
453 ActionSeq actions;
454 while (parseIter)
455 compile (parseIter, actions);
456 return actions;
457 }
458
459 private:
460 template<class PAR>
461 void
462 compile (PAR& parseIter, ActionSeq& actions)
463 {
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); };
468
469 // Support for bracketing constructs (if / for)
470 auto beginIdx = [&]{ return scope_.empty()? 0 : scope_.top().begin; }; // Index of action where scope was opened
471 auto scopeKey = [&]{ return valid(beginIdx())? actions[beginIdx()].val : "";}; // Key controlling the if-/for-Scope
472 auto keyMatch = [&]{ return isnil(parseIter->key) or parseIter->key == scopeKey(); }; // Key matches in opening and closing tag
473 auto clauseMatch = [&](Clause c){ return not scope_.empty() and scope_.top().clause == c; }; // Kind of closing tag matches innermost scope
474 auto scopeMatch = [&](Clause c){ return clauseMatch(c) and keyMatch(); };
475
476 auto lead = [&]{ return parseIter->lead; };
477 auto clashLead = [&]{ return actions[scope_.top().after - 1].val; }; // (for diagnostics: lead before a conflicting other "else")
478 auto abbrev = [&](auto s){ return s.length()<16? s : s.substr(s.length()-15); }; // (shorten lead display to 15 chars)
479
480 // Syntax / consistency checks...
481 auto __requireKey = [&](string descr)
482 {
483 if (isnil (parseIter->key))
484 throw error::Invalid{_Fmt{"Tag without key: ...%s${%s |↯|}"}
485 % abbrev(lead()) % descr
486 }; };
487 auto __checkBalanced = [&](Clause c)
488 {
489 if (not scopeMatch(c))
490 throw error::Invalid{_Fmt{"Unbalanced Logic: expect ${end %s %s}"
491 " -- found ...%s${end |↯|%s %s}"}
492 % scopeClause() % scopeKey()
493 % abbrev(lead())
494 % clause(c) % parseIter->key
495 }; };
496 auto __checkInScope = [&] {
497 if (scope_.empty())
498 throw error::Invalid{_Fmt{"Misplaced ...%s|↯|${else}"}
499 % abbrev(lead())};
500 };
501 auto __checkNoDup = [&] {
502 if (scope_.top().after != 0)
503 throw error::Invalid{_Fmt{"Conflicting ...%s${else} ⟷ ...%s|↯|${else}"}
504 % abbrev(clashLead()) % abbrev(lead())};
505 };
506 auto __checkClosed = [&] {
507 if (not scope_.empty())
508 throw error::Invalid{_Fmt{"Unclosed Logic tags: |↯|${end %s %s} missing"}
509 % scopeClause() % scopeKey()};
510 };
511
512 // Primitives used for code generation....
513 auto add = [&](Code c, string v){ actions.push_back (Action{c,v});};
514 auto addCode = [&](Code c) { add ( c, parseIter->key); }; // add code token and transfer key picked up by parser
515 auto addLead = [&] { add (TEXT, string{parseIter->lead}); }; // add TEXT token to represent the static part before this tag
516 auto openScope = [&](Clause c){ scope_.push (ParseCtx{c, currIDX()}); }; // start nested scope for bracketing construct (if / for)
517 auto closeScope = [&] { scope_.pop(); }; // close innermost nested scope
518
519 auto linkElseToStart = [&]{ actions[beginIdx()].refIDX = currIDX(); }; // link the start position of the else-branch into opening logic code
520 auto markJumpInScope = [&]{ scope_.top().after = currIDX(); }; // memorise jump before else-branch for later linkage
521 auto linkLoopBack = [&]{ actions.back().refIDX = scope_.top().begin; }; // fill in the back-jump position at loop end
522 auto linkJumpToNext = [&]{ actions[scope_.top().after].refIDX = currIDX(); }; // link jump to the position after the end of the logic bracket
523
524 auto hasElse = [&]{ return scope_.top().after != 0; }; // a jump code to link was only marked if there was an else tag
525
526 using text_template::TagSyntax;
527
528 /* === Code Generation === */
529 switch (parseIter->syntax) {
530 case TagSyntax::ESCAPE:
531 addLead();
532 break;
533 case TagSyntax::KEYID:
534 __requireKey("<placeholder>");
535 addLead();
536 addCode(KEY);
537 break;
538 case TagSyntax::IF:
539 __requireKey("if <conditional>");
540 addLead();
541 openScope(IF);
542 addCode(COND);
543 break;
544 case TagSyntax::END_IF:
545 addLead();
546 __checkBalanced(IF);
547 if (hasElse())
548 linkJumpToNext();
549 else
550 linkElseToStart();
551 closeScope();
552 break;
553 case TagSyntax::FOR:
554 __requireKey("for <data-id>");
555 addLead();
556 openScope(FOR);
557 addCode(ITER);
558 break;
559 case TagSyntax::END_FOR:
560 addLead();
561 __checkBalanced(FOR);
562 if (hasElse())
563 linkJumpToNext();
564 else
565 { // no else-branch; end active loop here
566 addCode(LOOP);
567 linkLoopBack();
568 linkElseToStart(); // jump behind when iteration turns out empty
569 }
570 closeScope();
571 break;
572 case TagSyntax::ELSE:
573 addLead();
574 __checkInScope();
575 __checkNoDup();
576 if (IF == scope_.top().clause)
577 {
578 markJumpInScope();
579 addCode(JUMP);
580 linkElseToStart();
581 }
582 else
583 {
584 addCode(LOOP);
585 linkLoopBack();
586 markJumpInScope();
587 addCode(JUMP);
588 linkElseToStart(); // jump to else-block when iteration turns out empty
589 }
590 break;
591 default:
592 NOTREACHED ("uncovered TagSyntax keyword while compiling a TextTemplate.");
593 }
594
595 StrView tail = parseIter->tail;
596 ++parseIter;
597 if (not parseIter)
598 {//add final action to supply text after last active tag
599 add (TEXT, string{tail});
600 __checkClosed();
601 }
602 }
603 };
604
605 inline TextTemplate::ActionSeq
606 TextTemplate::compile (string const& spec)
607 {
608 ActionSeq code = ActionCompiler().buildActions (text_template::parse (spec));
609 if (isnil (code))
610 throw error::Invalid ("TextTemplate spec without active placeholders.");
611 return code;
612 }
613
614
615
616
617
618
619 /* ======= preconfigured data bindings ======= */
620
621 namespace text_template {
622
623 template<class DAT, typename SEL>
624 struct DataSource
625 {
626 static_assert (not sizeof(DAT),
627 "unable to bind this data source "
628 "for TextTemplate instantiation");
629
630 DataSource (DAT const&);
631 };
632
633 using MapS = std::map<string,string>;
634
653 template<>
654 struct DataSource<MapS>
655 {
656 MapS const * data_{nullptr};
657 string keyPrefix_{};
658
659 bool isSubScope() { return not isnil (keyPrefix_); }
660
661 DataSource() = default;
662 DataSource(MapS const& map)
663 : data_{&map}
664 { }
665
666
667 using Value = std::string_view;
668 using Iter = decltype(iterNestedKeys("",""));
669
670 bool
671 contains (string key)
672 {
673 return (isSubScope() and util::contains (*data_, keyPrefix_+key))
674 or util::contains (*data_, key);
675 }
676
677 Value
678 retrieveContent (string key)
679 {
680 MapS::const_iterator elm;
681 if (isSubScope())
682 {
683 elm = data_->find (keyPrefix_+key);
684 if (elm == data_->end())
685 elm = data_->find (key);
686 }
687 else
688 elm = data_->find (key);
689 ENSURE (elm != data_->end());
690 return elm->second;
691 }
692
693 Iter
694 getSequence (string key)
695 {
696 if (not contains(key))
697 return Iter{};
698 else
699 return iterNestedKeys (key, retrieveContent(key));
700 }
701
702 DataSource
703 openContext (Iter& iter)
704 {
705 REQUIRE (iter);
706 DataSource nested{*this};
707 nested.keyPrefix_ += *iter;
708 return nested;
709 }
710 };
711
712
713 using PairS = std::pair<string,string>;
714
716 template<>
717 struct DataSource<string>
718 : DataSource<MapS>
719 {
720 std::shared_ptr<MapS> spec_;
721
722 DataSource (string const& dataSpec)
723 : spec_{new MapS}
724 {
725 data_ = spec_.get();
726 explore (iterBindingSeq (dataSpec))
727 .foreach([this](PairS const& bind){ spec_->insert (bind); });
728 }
729
730 DataSource
731 openContext (Iter& iter)
732 {
733 DataSource nested(*this);
734 auto nestedBase = DataSource<MapS>::openContext (iter);
735 nested.keyPrefix_ = nestedBase.keyPrefix_;
736 return nested;
737 }
738 };
739
744 template<class STR, typename = meta::enable_if<meta::is_StringLike<STR>> >
745 DataSource(STR const&) -> DataSource<string>;
746
747 }// namespace text_template
748
749
750
751
752
753 /* ======= implementation of the instantiation state ======= */
754
761 template<class SRC>
762 inline auto
763 TextTemplate::Action::instantiate (InstanceCore<SRC>& core) const
764 {
765 using Result = decltype (core.getContent(val));
766 switch (code) {
767 case TEXT:
768 return Result(val);
769 case KEY:
770 return core.getContent (val);
771 case COND:
772 return core.conditional (val)? core.reInstatiate() // next is the conditional content
773 : core.reInstatiate(refIDX); // points to start of else-block (or after)
774 case JUMP:
775 return core.reInstatiate(refIDX);
776 case ITER:
777 return core.openIteration(val)? core.reInstatiate() // looping initiated => continue with next
778 : core.reInstatiate(refIDX); // points to start of else-block (or after)
779 case LOOP:
780 return core.loopFurther() ? core.reInstatiate(refIDX+1) // start with one after the loop opening
781 : core.reInstatiate(); // continue with next -> jump over else-block
782 default:
783 NOTREACHED ("uncovered Activity verb in activation function.");
784 }
785 }
786
787
788
789 template<class SRC>
790 TextTemplate::InstanceCore<SRC>::InstanceCore (TextTemplate::ActionSeq const& actions, SRC s)
791 : dataSrc_{s}
792 , actionIter_{actions}
793 , ctxStack_{}
794 , rendered_{}
795 {
796 rendered_ = instantiateNext();
797 }
798
804 template<class SRC>
805 inline bool
806 TextTemplate::InstanceCore<SRC>::checkPoint() const
807 {
808 return bool(actionIter_);
809 }
810
811 template<class SRC>
812 inline auto&
813 TextTemplate::InstanceCore<SRC>::yield() const
814 {
815 return unConst(this)->rendered_;
816 }
817
818 template<class SRC>
819 inline void
820 TextTemplate::InstanceCore<SRC>::iterNext()
821 {
822 ++actionIter_;
823 rendered_ = instantiateNext();
824 }
825
826
828 template<class SRC>
829 inline SRC::Value
830 TextTemplate::InstanceCore<SRC>::instantiateNext()
831 {
832 return actionIter_? actionIter_->instantiate(*this)
833 : Value{};
834 }
835
842 template<class SRC>
843 inline SRC::Value
844 TextTemplate::InstanceCore<SRC>::reInstatiate (Idx nextCode)
845 {
846 if (nextCode == Idx(-1))
847 ++actionIter_;
848 else
849 actionIter_.setIDX (nextCode);
850 return instantiateNext();
851 }
852
854 template<class SRC>
855 inline SRC::Value
856 TextTemplate::InstanceCore<SRC>::getContent (string key)
857 {
858 static Value nil{};
859 return dataSrc_.contains(key)? dataSrc_.retrieveContent(key) : nil;
860 }
861
863 template<class SRC>
864 inline bool
865 TextTemplate::InstanceCore<SRC>::conditional (string key)
866 {
867 return not util::isNo (string{getContent (key)});
868 }
869
881 template<class SRC>
882 inline bool
883 TextTemplate::InstanceCore<SRC>::openIteration (string key)
884 {
885 if (conditional (key))
886 if (DataCtxIter dataIter = dataSrc_.getSequence(key))
887 {
888 ctxStack_.push (NestedCtx{move (dataIter)
889 ,dataSrc_});
890 focusNested();
891 return true;
892 }
893 return false;
894 }
895
902 template<class SRC>
903 inline bool
904 TextTemplate::InstanceCore<SRC>::loopFurther()
905 {
906 DataCtxIter& dataIter = ctxStack_.top().first;
907 ++dataIter;
908 if (dataIter)
909 { // open next nested context *from enclosing context*
910 focusNested();
911 return true;
912 }
913 else
914 { // restore original data context
915 using std::swap;
916 swap (dataSrc_, ctxStack_.top().second);
917 ctxStack_.pop();
918 return false;
919 }
920 }
921
932 template<class SRC>
933 inline void
934 TextTemplate::InstanceCore<SRC>::focusNested()
935 {
936 REQUIRE (not ctxStack_.empty());
937 NestedCtx& innermostScope = ctxStack_.top();
938 DataCtxIter& currentDataItem = innermostScope.first;
939 SRC& parentDataSrc = innermostScope.second;
940
941 this->dataSrc_ = parentDataSrc.openContext (currentDataItem);
942 }
943
944
945
946
947
953 template<class DAT>
954 inline auto
955 TextTemplate::submit (DAT const& data) const
956 {
957 return explore (InstanceCore{actions_, text_template::DataSource(data)});
958 }
959
961 template<class DAT>
962 inline string
963 TextTemplate::render (DAT const& data) const
964 {
965 return util::join (submit (data), "");
966 }
967
969 template<class DAT>
970 inline string
971 TextTemplate::apply (string spec, DAT const& data)
972 {
973 return TextTemplate(spec).render (data);
974 }
975
977 inline auto
978 TextTemplate::keys() const
979 {
980 return explore (actions_)
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; });
983 }
984
985
986}// namespace lib
987#endif /*LIB_TEXT_TEMPLATE_H*/
Derived specific exceptions within Lumiera's exception hierarchy.
Definition error.hpp:193
Types marked with this mix-in may be moved but not copied.
Definition nocopy.hpp:50
A front-end for using printf-style formatting.
Lumiera error handling (C++ interface).
Front-end for printf-style string template interpolation.
Collection of small helpers and convenience shortcuts for diagnostics & formatting.
Iterator-style access handle to a referred container with subscript index.
Building tree expanding and backtracking evaluations within hierarchical scopes.
_TransformIterT< IT, FUN >::Iter transform(IT &&source, FUN processingFunc)
pipes a given Lumiera Forward Iterator through a transformation function and wraps the resulting tran...
constexpr decltype(auto) apply(FUN &&f, TUP &&tup) noexcept(can_nothrow_invoke_tup< FUN, TUP >)
Replacement for std::apply — yet applicable to tuple-like custom types.
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
Definition error.hpp:207
LumieraError< LERR_(INVALID)> Invalid
Definition error.hpp:211
@ TEXT
represents text content
Test runner and basic definitions for tests.
std::string_view StrView
Definition parse.hpp:174
bool isNo(string const &textForm) noexcept
check if the given text is empty or can be interpreted as rejection (bool false)-
Definition util.cpp:115
bool contains(MAP &map, typename MAP::key_type const &key)
shortcut for containment test on a map
Definition util.hpp:230
OBJ * unConst(const OBJ *)
shortcut to save some typing when having to define const and non-const variants of member functions
Definition util.hpp:358
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
Definition regex.hpp:42
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...