1 /**
2  * Mustache template engine for D
3  *
4  * Implemented according to <a href="http://mustache.github.com/mustache.5.html">mustach(5)</a>.
5  *
6  * Copyright: Copyright Masahiro Nakagawa 2011-.
7  * License:   <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
8  * Authors:   Masahiro Nakagawa
9  */
10 module mustache;
11 
12 import std.algorithm : all;
13 import std.array;    // empty, back, popBack, appender
14 import std.conv;     // to
15 import std.datetime; // SysTime (I think std.file should import std.datetime as public)
16 import std.file;     // read, timeLastModified
17 import std.path;     // buildPath
18 import std.range;    // isOutputRange
19 import std..string;   // strip, chomp, stripLeft
20 import std.traits;   // isSomeString, isAssociativeArray
21 
22 static import std.ascii; // isWhite;
23 
24 version(unittest) import core.thread;
25 
26 
27 /**
28  * Exception for Mustache
29  */
30 class MustacheException : Exception
31 {
32     this(string messaage)
33     {
34         super(messaage);
35     }
36 }
37 
38 
39 /**
40  * Core implementation of Mustache
41  *
42  * $(D_PARAM String) parameter means a string type to render.
43  *
44  * Example:
45  * -----
46  * alias MustacheEngine!(string) Mustache;
47  *
48  * Mustache mustache;
49  * auto context = new Mustache.Context;
50  *
51  * context["name"]  = "Chris";
52  * context["value"] = 10000;
53  * context["taxed_value"] = 10000 - (10000 * 0.4);
54  * context.useSection("in_ca");
55  *
56  * write(mustache.render("sample", context));
57  * -----
58  * sample.mustache:
59  * -----
60  * Hello {{name}}
61  * You have just won ${{value}}!
62  * {{#in_ca}}
63  * Well, ${{taxed_value}}, after taxes.
64  * {{/in_ca}}
65  * -----
66  * Output:
67  * -----
68  * Hello Chris
69  * You have just won $10000!
70  * Well, $6000, after taxes.
71  * -----
72  */
73 struct MustacheEngine(String = string) if (isSomeString!(String))
74 {
75     static assert(!is(String == wstring), "wstring is unsupported. It's a buggy!");
76 
77 
78   public:
79     alias String delegate(String) Handler;
80     alias string delegate(string) FindPath;
81 
82 
83     /**
84      * Cache level for compile result
85      */
86     static enum CacheLevel
87     {
88         no,     /// No caching
89         check,  /// Caches compiled result and checks the freshness of template
90         once    /// Caches compiled result but not check the freshness of template
91     }
92 
93 
94     /**
95      * Options for rendering
96      */
97     static struct Option
98     {
99         string     ext   = "mustache";        /// template file extenstion
100         string     path  = ".";               /// root path for template file searching
101         FindPath   findPath;                  /// dynamically finds the path for a name
102         CacheLevel level = CacheLevel.check;  /// See CacheLevel
103         Handler    handler;                   /// Callback handler for unknown name
104     }
105 
106 
107     /**
108      * Mustache context for setting values
109      *
110      * Variable:
111      * -----
112      * //{{name}} to "Chris"
113      * context["name"] = "Chirs"
114      * -----
115      *
116      * Lists section("addSubContext" name is drived from ctemplate's API):
117      * -----
118      * //{{#repo}}
119      * //<b>{{name}}</b>
120      * //{{/repo}}
121      * //  to
122      * //<b>resque</b>
123      * //<b>hub</b>
124      * //<b>rip</b>
125      * foreach (name; ["resque", "hub", "rip"]) {
126      *     auto sub = context.addSubContext("repo");
127      *     sub["name"] = name;
128      * }
129      * -----
130      *
131      * Variable section:
132      * -----
133      * //{{#person?}}Hi {{name}}{{/person?}} to "Hi Jon"
134      * context["person?"] = ["name" : "Jon"];
135      * -----
136      *
137      * Lambdas section:
138      * -----
139      * //{{#wrapped}}awesome{{/wrapped}} to "<b>awesome</b>"
140      * context["Wrapped"] = (string str) { return "<b>" ~ str ~ "</b>"; };
141      * -----
142      *
143      * Inverted section:
144      * -----
145      * //{{#repo}}<b>{{name}}</b>{{/repo}}
146      * //{{^repo}}No repos :({{/repo}}
147      * //  to
148      * //No repos :(
149      * context["foo"] = "bar";  // not set to "repo"
150      * -----
151      */
152     static final class Context
153     {
154       private:
155         enum SectionType
156         {
157             nil, use, var, func, list
158         }
159 
160         struct Section
161         {
162             SectionType type;
163 
164             union
165             {
166                 String[String]          var;
167                 String delegate(String) func;  // func type is String delegate(String) delegate()?
168                 Context[]               list;
169             }
170 
171             @trusted nothrow
172             {
173                 this(bool u)
174                 {
175                     type = SectionType.use;
176                 }
177 
178                 this(String[String] v)
179                 {
180                     type = SectionType.var;
181                     var  = v;
182                 }
183 
184                 this(String delegate(String) f)
185                 {
186                     type = SectionType.func;
187                     func = f;
188                 }
189 
190                 this(Context c)
191                 {
192                     type = SectionType.list;
193                     list = [c];
194                 }
195 
196                 this(Context[] c)
197                 {
198                     type = SectionType.list;
199                     list = c;
200                 }
201             }
202 
203             /* nothrow : AA's length is not nothrow */
204             @trusted @property
205             bool empty() const
206             {
207                 final switch (type) {
208                 case SectionType.nil:
209                     return true;
210                 case SectionType.use:
211                     return false;
212                 case SectionType.var:
213                     return !var.length;  // Why?
214                 case SectionType.func:
215                     return func is null;
216                 case SectionType.list:
217                     return !list.length;
218                 }
219             }
220 
221             /* Convenience function */
222             @safe @property
223             static Section nil() nothrow
224             {
225                 Section result;
226                 result.type = SectionType.nil;
227                 return result;
228             }
229         }
230 
231         const Context   parent;
232         String[String]  variables;
233         Section[String] sections;
234 
235 
236       public:
237         @safe
238         this(const Context context = null) nothrow
239         {
240             parent = context;
241         }
242 
243         /**
244          * Gets $(D_PARAM key)'s value. This method does not search Section.
245          *
246          * Params:
247          *  key = key string to search
248          *
249          * Returns:
250          *  a $(D_PARAM key) associated value.
251          *
252          * Throws:
253          *  a RangeError if $(D_PARAM key) does not exist.
254          */
255         @safe
256         String opIndex(in String key) const nothrow
257         {
258             return variables[key];
259         }
260 
261         /**
262          * Assigns $(D_PARAM value)(automatically convert to String) to $(D_PARAM key) field.
263          *
264          * If you try to assign associative array or delegate,
265          * This method assigns $(D_PARAM value) as Section.
266          *
267          * Arrays of Contexts are accepted, too.
268          *
269          * Params:
270          *  value = some type value to assign
271          *  key   = key string to assign
272          */
273         @trusted
274         void opIndexAssign(T)(T value, in String key)
275         {
276             static if (isAssociativeArray!(T))
277             {
278                 static if (is(T V : V[K], K : String))
279                 {
280                     String[String] aa;
281 
282                     static if (is(V == String))
283                         aa = value;
284                     else
285                         foreach (k, v; value) aa[k] = to!String(v);
286 
287                     sections[key] = Section(aa);
288                 }
289                 else static assert(false, "Non-supported Associative Array type");
290             }
291             else static if (isCallable!T)
292             {
293                 import std.functional : toDelegate;
294 
295                 auto v = toDelegate(value);
296                 static if (is(typeof(v) D == S delegate(S), S : String))
297                     sections[key] = Section(v);
298                 else static assert(false, "Non-supported delegate type");
299             }
300             else static if (isArray!T && !isSomeString!T)
301             {
302                 static if (is(T : Context[]))
303                     sections[key] = Section(value);
304                 else static assert(false, "Non-supported array type");
305             }
306             else
307             {
308                 variables[key] = to!String(value);
309             }
310         }
311 
312         /**
313          * Enable $(D_PARAM key)'s section.
314          *
315          * Params:
316          *  key = key string to enable
317          *
318          * NOTE:
319          *  I don't like this method, but D's typing can't well-handle Ruby's typing.
320          */
321         @safe
322         void useSection(in String key)
323         {
324             sections[key] = Section(true);
325         }
326 
327         /**
328          * Adds new context to $(D_PARAM key)'s section. This method overwrites with
329          * list type if you already assigned other type to $(D_PARAM key)'s section.
330          *
331          * Params:
332          *  key  = key string to add
333          *  size = reserve size for avoiding reallocation
334          *
335          * Returns:
336          *  new Context object that added to $(D_PARAM key) section list.
337          */
338         @trusted
339         Context addSubContext(in String key, lazy size_t size = 1)
340         {
341             auto c = new Context(this);
342             auto p = key in sections;
343             if (!p || p.type != SectionType.list) {
344                 sections[key] = Section(c);
345                 sections[key].list.reserve(size);
346             } else {
347                 sections[key].list ~= c;
348             }
349 
350             return c;
351         }
352 
353 
354       private:
355         /*
356          * Fetches $(D_PARAM)'s value. This method follows parent context.
357          *
358          * Params:
359          *  key = key string to fetch
360          *
361          * Returns:
362          *  a $(D_PARAM key) associated value. null if key does not exist.
363          */
364         @trusted
365         String fetch(in String[] key, lazy Handler handler = null) const
366         {
367             assert(key.length > 0);
368 
369             if (key.length == 1) {
370                 auto result = key[0] in variables;
371 
372                 if (result !is null)
373                     return *result;
374 
375                 if (parent !is null)
376                     return parent.fetch(key, handler);
377             } else {
378                 auto contexts = fetchList(key[0..$-1]);
379                 foreach (c; contexts) {
380                     auto result = key[$-1] in c.variables;
381 
382                     if (result !is null)
383                         return *result;
384                 }
385             }
386 
387             return handler is null ? null : handler()(keyToString(key));
388         }
389 
390         @trusted
391         const(Section) fetchSection()(in String[] key) const /* nothrow */
392         {
393             assert(key.length > 0);
394 
395             // Ascend context tree to find the key's beginning
396             auto currentSection = key[0] in sections;
397             if (currentSection is null) {
398                 if (parent is null)
399                     return Section.nil;
400 
401                 return parent.fetchSection(key);
402             }
403 
404             // Decend context tree to match the rest of the key
405             size_t keyIndex = 0;
406             while (currentSection) {
407                 // Matched the entire key?
408                 if (keyIndex == key.length-1)
409                     return currentSection.empty ? Section.nil : *currentSection;
410 
411                 if (currentSection.type != SectionType.list)
412                     return Section.nil; // Can't decend any further
413 
414                 // Find next part of key
415                 keyIndex++;
416                 foreach (c; currentSection.list)
417                 {
418                     currentSection = key[keyIndex] in c.sections;
419                     if (currentSection)
420                         break;
421                 }
422             }
423 
424             return Section.nil;
425         }
426 
427         @trusted
428         const(Result) fetchSection(Result, SectionType type, string name)(in String[] key) const /* nothrow */
429         {
430             auto result = fetchSection(key);
431             if (result.type == type)
432                 return result.empty ? null : mixin("result." ~ to!string(type));
433 
434             return null;
435         }
436 
437         alias fetchSection!(String[String],          SectionType.var,  "Var")  fetchVar;
438         alias fetchSection!(Context[],               SectionType.list, "List") fetchList;
439         alias fetchSection!(String delegate(String), SectionType.func, "Func") fetchFunc;
440     }
441 
442     unittest
443     {
444         Context context = new Context();
445 
446         context["name"] = "Red Bull";
447         assert(context["name"] == "Red Bull");
448         context["price"] = 275;
449         assert(context["price"] == "275");
450 
451         { // list
452             foreach (i; 100..105) {
453                 auto sub = context.addSubContext("sub");
454                 sub["num"] = i;
455 
456                 foreach (b; [true, false]) {
457                     auto subsub = sub.addSubContext("subsub");
458                     subsub["To be or not to be"] = b;
459                 }
460             }
461 
462             foreach (i, sub; context.fetchList(["sub"])) {
463                 assert(sub.fetch(["name"]) == "Red Bull");
464                 assert(sub["num"] == to!String(i + 100));
465 
466                 foreach (j, subsub; sub.fetchList(["subsub"])) {
467                     assert(subsub.fetch(["price"]) == to!String(275));
468                     assert(subsub["To be or not to be"] == to!String(j == 0));
469                 }
470             }
471         }
472         { // variable
473             String[String] aa = ["name" : "Ritsu"];
474 
475             context["Value"] = aa;
476             assert(context.fetchVar(["Value"]) == cast(const)aa);
477         }
478         { // func
479             auto func = function (String str) { return "<b>" ~ str ~ "</b>"; };
480 
481             context["Wrapped"] = func;
482             assert(context.fetchFunc(["Wrapped"])("Ritsu") == func("Ritsu"));
483         }
484         { // handler
485             Handler fixme = delegate String(String s) { assert(s=="unknown"); return "FIXME"; };
486             Handler error = delegate String(String s) { assert(s=="unknown"); throw new MustacheException("Unknow"); };
487 
488             assert(context.fetch(["unknown"]) == "");
489             assert(context.fetch(["unknown"], fixme) == "FIXME");
490             try {
491                 assert(context.fetch(["unknown"], error) == "");
492                 assert(false);
493             } catch (MustacheException e) { }
494         }
495         { // subcontext
496             auto sub = new Context();
497             sub["num"] = 42;
498             context["a"] = [sub];
499 
500             auto list = context.fetchList(["a"]);
501             assert(list.length == 1);
502             foreach (i, s; list)
503                 assert(s["num"] == to!String(42));
504         }
505     }
506 
507 
508   private:
509     // Internal cache
510     struct Cache
511     {
512         Node[]  compiled;
513         SysTime modified;
514     }
515 
516     Option        option_;
517     Cache[string] caches_;
518 
519 
520   public:
521     @safe
522     this(Option option) nothrow
523     {
524         option_ = option;
525     }
526 
527     @property @safe nothrow
528     {
529         /**
530          * Property for template extenstion
531          */
532         const(string) ext() const
533         {
534             return option_.ext;
535         }
536 
537         /// ditto
538         void ext(string ext)
539         {
540             option_.ext = ext;
541         }
542 
543         /**
544          * Property for template searche path
545          */
546         const(string) path() const
547         {
548             return option_.path;
549         }
550 
551         /// ditto
552         void path(string path)
553         {
554             option_.path = path;
555         }
556 
557         /**
558          * Property for callback to dynamically search path.
559          * The result of the delegate should return the full path for
560          * the given name.
561          */
562         FindPath findPath() const
563         {
564             return option_.findPath;
565         }
566 
567         /// ditto
568         void findPath(FindPath findPath)
569         {
570             option_.findPath = findPath;
571         }
572 
573         /**
574          * Property for cache level
575          */
576         const(CacheLevel) level() const
577         {
578             return option_.level;
579         }
580 
581         /// ditto
582         void level(CacheLevel level)
583         {
584             option_.level = level;
585         }
586 
587         /**
588          * Property for callback handler
589          */
590         const(Handler) handler() const
591         {
592             return option_.handler;
593         }
594 
595         /// ditto
596         void handler(Handler handler)
597         {
598             option_.handler = handler;
599         }
600     }
601 
602     /**
603      * Clears the intenal cache.
604      * Useful for forcing reloads when using CacheLevel.once.
605      */
606     @safe
607     void clearCache()
608     {
609         caches_ = null;
610     }
611 
612     /**
613      * Renders $(D_PARAM name) template with $(D_PARAM context).
614      *
615      * This method stores compile result in memory if you set check or once CacheLevel.
616      *
617      * Params:
618      *  name    = template name without extenstion
619      *  context = Mustache context for rendering
620      *
621      * Returns:
622      *  rendered result.
623      *
624      * Throws:
625      *  object.Exception if String alignment is mismatched from template file.
626      */
627     String render()(in string name, in Context context)
628     {
629         auto sink = appender!String();
630         render(name, context, sink);
631         return sink.data;
632     }
633 
634     /**
635     * OutputRange version of $(D render).
636     */
637     void render(Sink)(in string name, in Context context, ref Sink sink)
638         if(isOutputRange!(Sink, String))
639     {
640         /*
641          * Helper for file reading
642          *
643          * Throws:
644          *  object.Exception if alignment is mismatched.
645          */
646         @trusted
647         static String readFile(string file)
648         {
649             // cast checks character encoding alignment.
650             return cast(String)read(file);
651         }
652 
653         string file;
654         if (option_.findPath) {
655             file = option_.findPath(name);
656         } else {
657             file = buildPath(option_.path, name ~ "." ~ option_.ext);
658         }
659         Node[] nodes;
660 
661         final switch (option_.level) {
662         case CacheLevel.no:
663             nodes = compile(readFile(file));
664             break;
665         case CacheLevel.check:
666             auto t = timeLastModified(file);
667             auto p = file in caches_;
668             if (!p || t > p.modified)
669                 caches_[file] = Cache(compile(readFile(file)), t);
670             nodes = caches_[file].compiled;
671             break;
672         case CacheLevel.once:
673             if (file !in caches_)
674                 caches_[file] = Cache(compile(readFile(file)), SysTime.min);
675             nodes = caches_[file].compiled;
676             break;
677         }
678 
679         renderImpl(nodes, context, sink);
680     }
681 
682     /**
683      * string version of $(D render).
684      */
685     String renderString()(in String src, in Context context)
686     {
687         auto sink = appender!String();
688         renderString(src, context, sink);
689         return sink.data;
690     }
691 
692     /**
693      * string/OutputRange version of $(D render).
694      */
695     void renderString(Sink)(in String src, in Context context, ref Sink sink)
696         if(isOutputRange!(Sink, String))
697     {
698         renderImpl(compile(src), context, sink);
699     }
700 
701 
702   private:
703     /*
704      * Implemention of render function.
705      */
706     void renderImpl(Sink)(in Node[] nodes, in Context context, ref Sink sink)
707         if(isOutputRange!(Sink, String))
708     {
709         // helper for HTML escape(original function from std.xml.encode)
710         static void encode(in String text, ref Sink sink)
711         {
712             size_t index;
713 
714             foreach (i, c; text) {
715                 String temp;
716 
717                 switch (c) {
718                 case '&': temp = "&amp;";  break;
719                 case '"': temp = "&quot;"; break;
720                 case '<': temp = "&lt;";   break;
721                 case '>': temp = "&gt;";   break;
722                 default: continue;
723                 }
724 
725                 sink.put(text[index .. i]);
726                 sink.put(temp);
727                 index = i + 1;
728             }
729 
730             sink.put(text[index .. $]);
731         }
732 
733         foreach (ref node; nodes) {
734             final switch (node.type) {
735             case NodeType.text:
736                 sink.put(node.text);
737                 break;
738             case NodeType.var:
739                 auto value = context.fetch(node.key, option_.handler);
740                 if (value)
741                 {
742                     if(node.flag)
743                         sink.put(value);
744                     else
745                         encode(value, sink);
746                 }
747                 break;
748             case NodeType.section:
749                 auto section = context.fetchSection(node.key);
750                 final switch (section.type) {
751                 case Context.SectionType.nil:
752                     if (node.flag)
753                         renderImpl(node.childs, context, sink);
754                     break;
755                 case Context.SectionType.use:
756                     if (!node.flag)
757                         renderImpl(node.childs, context, sink);
758                     break;
759                 case Context.SectionType.var:
760                     auto var = section.var;
761                     auto sub = new Context(context);
762                     foreach (k, v; var)
763                         sub[k] = v;
764                     renderImpl(node.childs, sub, sink);
765                     break;
766                 case Context.SectionType.func:
767                     auto func = section.func;
768                     renderImpl(compile(func(node.source)), context, sink);
769                     break;
770                 case Context.SectionType.list:
771                     auto list = section.list;
772                     if (!node.flag) {
773                         foreach (sub; list)
774                             renderImpl(node.childs, sub, sink);
775                     }
776                     break;
777                 }
778                 break;
779             case NodeType.partial:
780                 render(to!string(node.key.front), context, sink);
781                 break;
782             }
783         }
784     }
785 
786 
787     unittest
788     {
789         MustacheEngine!(String) m;
790         auto render = (String str, Context c) => m.renderString(str, c);
791 
792         { // var
793             auto context = new Context;
794             context["name"] = "Ritsu & Mio";
795 
796             assert(render("Hello {{name}}",   context) == "Hello Ritsu &amp; Mio");
797             assert(render("Hello {{&name}}",  context) == "Hello Ritsu & Mio");
798             assert(render("Hello {{{name}}}", context) == "Hello Ritsu & Mio");
799         }
800         { // var with handler
801             auto context = new Context;
802             context["name"] = "Ritsu & Mio";
803 
804             m.handler = delegate String(String s) { assert(s=="unknown"); return "FIXME"; };
805             assert(render("Hello {{unknown}}", context) == "Hello FIXME");
806 
807             m.handler = delegate String(String s) { assert(s=="unknown"); throw new MustacheException("Unknow"); };
808             try {
809                 assert(render("Hello {{&unknown}}", context) == "Hello Ritsu & Mio");
810                 assert(false);
811             } catch (MustacheException e) {}
812 
813             m.handler = null;
814         }
815         { // list section
816             auto context = new Context;
817             foreach (name; ["resque", "hub", "rip"]) {
818                 auto sub = context.addSubContext("repo");
819                 sub["name"] = name;
820             }
821 
822             assert(render("{{#repo}}\n  <b>{{name}}</b>\n{{/repo}}", context) ==
823                    "  <b>resque</b>\n  <b>hub</b>\n  <b>rip</b>\n");
824         }
825         { // var section
826             auto context = new Context;
827             String[String] aa = ["name" : "Ritsu"];
828             context["person?"] = aa;
829 
830             assert(render("{{#person?}}  Hi {{name}}!\n{{/person?}}", context) ==
831                    "  Hi Ritsu!\n");
832         }
833         { // inverted section
834             {
835                 String temp  = "{{#repo}}\n<b>{{name}}</b>\n{{/repo}}\n{{^repo}}\nNo repos :(\n{{/repo}}\n";
836                 auto context = new Context;
837                 assert(render(temp, context) == "\nNo repos :(\n");
838 
839                 String[String] aa;
840                 context["person?"] = aa;
841                 assert(render(temp, context) == "\nNo repos :(\n");
842             }
843             {
844                 auto temp = "{{^section}}This shouldn't be seen.{{/section}}";
845                 auto context = new Context;
846                 context.addSubContext("section")["foo"] = "bar";
847                 assert(render(temp, context).empty);
848             }
849         }
850         { // comment
851             auto context = new Context;
852             assert(render("<h1>Today{{! ignore me }}.</h1>", context) == "<h1>Today.</h1>");
853         }
854         { // partial
855             std.file.write("user.mustache", to!String("<strong>{{name}}</strong>"));
856             scope(exit) std.file.remove("user.mustache");
857 
858             auto context = new Context;
859             foreach (name; ["Ritsu", "Mio"]) {
860                 auto sub = context.addSubContext("names");
861                 sub["name"] = name;
862             }
863 
864             assert(render("<h2>Names</h2>\n{{#names}}\n  {{> user}}\n{{/names}}\n", context) ==
865                    "<h2>Names</h2>\n  <strong>Ritsu</strong>\n  <strong>Mio</strong>\n");
866         }
867         { // dotted names
868             auto context = new Context;
869             context
870                 .addSubContext("a")
871                 .addSubContext("b")
872                 .addSubContext("c")
873                 .addSubContext("person")["name"] = "Ritsu";
874             context
875                 .addSubContext("b")
876                 .addSubContext("c")
877                 .addSubContext("person")["name"] = "Wrong";
878 
879             assert(render("Hello {{a.b.c.person.name}}",                  context) == "Hello Ritsu");
880             assert(render("Hello {{#a}}{{b.c.person.name}}{{/a}}",        context) == "Hello Ritsu");
881             assert(render("Hello {{# a . b }}{{c.person.name}}{{/a.b}}",  context) == "Hello Ritsu");
882         }
883         { // dotted names - context precedence
884             auto context = new Context;
885             context.addSubContext("a").addSubContext("b")["X"] = "Y";
886             context.addSubContext("b")["c"] = "ERROR";
887 
888             assert(render("-{{#a}}{{b.c}}{{/a}}-", context) == "--");
889         }
890         { // dotted names - broken chains
891             auto context = new Context;
892             context.addSubContext("a")["X"] = "Y";
893             assert(render("-{{a.b.c}}-", context) == "--");
894         }
895         { // dotted names - broken chain resolution
896             auto context = new Context;
897             context.addSubContext("a").addSubContext("b")["X"] = "Y";
898             context.addSubContext("c")["name"] = "ERROR";
899 
900             assert(render("-{{a.b.c.name}}-", context) == "--");
901         }
902     }
903 
904     /*
905      * Compiles $(D_PARAM src) into Intermediate Representation.
906      */
907     static Node[] compile(String src)
908     {
909         bool beforeNewline = true;
910 
911         // strip previous whitespace
912         bool fixWS(ref Node node)
913         {
914             // TODO: refactor and optimize with enum
915             if (node.type == NodeType.text) {
916                 if (beforeNewline) {
917                     if (all!(std.ascii.isWhite)(node.text)) {
918                         node.text = "";
919                         return true;
920                     }
921                 }
922 
923                 auto i = node.text.lastIndexOf('\n');
924                 if (i != -1) {
925                     if (all!(std.ascii.isWhite)(node.text[i + 1..$])) {
926                         node.text = node.text[0..i + 1];
927                         return true;
928                     }
929                 }
930             }
931 
932             return false;
933         }
934 
935         String sTag = "{{";
936         String eTag = "}}";
937 
938         void setDelimiter(String src)
939         {
940             auto i = src.indexOf(" ");
941             if (i == -1)
942                 throw new MustacheException("Delimiter tag needs whitespace");
943 
944             sTag = src[0..i];
945             eTag = src[i + 1..$].stripLeft();
946         }
947 
948         size_t getEnd(String src)
949         {
950             auto end = src.indexOf(eTag);
951             if (end == -1)
952                 throw new MustacheException("Mustache tag is not closed");
953 
954             return end;
955         }
956 
957         // State capturing for section
958         struct Memo
959         {
960             String[] key;
961             Node[]   nodes;
962             String   source;
963 
964             bool opEquals()(auto ref const Memo m) inout
965             {
966                 // Don't compare source because the internal
967                 // whitespace might be different
968                 return key == m.key && nodes == m.nodes;
969             }
970         }
971 
972         Node[] result;
973         Memo[] stack;   // for nested section
974         bool singleLineSection;
975 
976         while (true) {
977             if (singleLineSection) {
978                 src = chompPrefix(src, "\n");
979                 singleLineSection = false;
980             }
981 
982             auto hit = src.indexOf(sTag);
983             if (hit == -1) {  // rest template does not have tags
984                 if (src.length > 0)
985                     result ~= Node(src);
986                 break;
987             } else {
988                 if (hit > 0)
989                     result ~= Node(src[0..hit]);
990                 src = src[hit + sTag.length..$];
991             }
992 
993             size_t end;
994 
995             immutable type = src[0];
996             switch (type) {
997             case '#': case '^':
998                 src = src[1..$];
999                 auto key = parseKey(src, eTag, end);
1000 
1001                 if (result.length == 0) {  // for start of template
1002                     singleLineSection = true;
1003                 } else if (result.length > 0) {
1004                     if (src[end + eTag.length] == '\n') {
1005                         singleLineSection = fixWS(result[$ - 1]);
1006                         beforeNewline = false;
1007                     }
1008                 }
1009 
1010                 result ~= Node(NodeType.section, key, type == '^');
1011                 stack  ~= Memo(key, result, src[end + eTag.length..$]);
1012                 result  = null;
1013                 break;
1014             case '/':
1015                 src = src[1..$];
1016                 auto key = parseKey(src, eTag, end);
1017                 if (stack.empty)
1018                     throw new MustacheException(to!string(key) ~ " is unopened");
1019                 auto memo = stack.back; stack.popBack(); stack.assumeSafeAppend();
1020                 if (key != memo.key)
1021                     throw new MustacheException(to!string(key) ~ " is different from expected " ~ to!string(memo.key));
1022 
1023                 if (src.length == (end + eTag.length)) // for end of template
1024                     fixWS(result[$ - 1]);
1025                 if ((src.length > (end + eTag.length)) && (src[end + eTag.length] == '\n')) {
1026                     singleLineSection = fixWS(result[$ - 1]);
1027                     beforeNewline = false;
1028                 }
1029 
1030                 auto temp = result;
1031                 result = memo.nodes;
1032                 result[$ - 1].childs = temp;
1033                 result[$ - 1].source = memo.source[0..src.ptr - memo.source.ptr - 1 - eTag.length];
1034                 break;
1035             case '>':
1036                 // TODO: If option argument exists, this function can read and compile partial file.
1037                 end = getEnd(src);
1038                 result ~= Node(NodeType.partial, [src[1..end].strip()]);
1039                 break;
1040             case '=':
1041                 end = getEnd(src);
1042                 setDelimiter(src[1..end - 1]);
1043                 break;
1044             case '!':
1045                 end = getEnd(src);
1046                 break;
1047             case '{':
1048                 src = src[1..$];
1049                 auto key = parseKey(src, "}", end);
1050 
1051                 end += 1;
1052                 if (end >= src.length || !src[end..$].startsWith(eTag))
1053                     throw new MustacheException("Unescaped tag is not closed");
1054 
1055                 result ~= Node(NodeType.var, key, true);
1056                 break;
1057             case '&':
1058                 src = src[1..$];
1059                 auto key = parseKey(src, eTag, end);
1060                 result ~= Node(NodeType.var, key, true);
1061                 break;
1062             default:
1063                 auto key = parseKey(src, eTag, end);
1064                 result ~= Node(NodeType.var, key);
1065                 break;
1066             }
1067 
1068             src = src[end + eTag.length..$];
1069         }
1070 
1071         return result;
1072     }
1073 
1074     unittest
1075     {
1076         {  // text and unescape
1077             auto nodes = compile("Hello {{{name}}}");
1078             assert(nodes[0].type == NodeType.text);
1079             assert(nodes[0].text == "Hello ");
1080             assert(nodes[1].type == NodeType.var);
1081             assert(nodes[1].key  == ["name"]);
1082             assert(nodes[1].flag == true);
1083         }
1084         {  // section and escape
1085             auto nodes = compile("{{#in_ca}}\nWell, ${{taxed_value}}, after taxes.\n{{/in_ca}}\n");
1086             assert(nodes[0].type   == NodeType.section);
1087             assert(nodes[0].key    == ["in_ca"]);
1088             assert(nodes[0].flag   == false);
1089             assert(nodes[0].source == "\nWell, ${{taxed_value}}, after taxes.\n");
1090 
1091             auto childs = nodes[0].childs;
1092             assert(childs[0].type == NodeType.text);
1093             assert(childs[0].text == "Well, $");
1094             assert(childs[1].type == NodeType.var);
1095             assert(childs[1].key  == ["taxed_value"]);
1096             assert(childs[1].flag == false);
1097             assert(childs[2].type == NodeType.text);
1098             assert(childs[2].text == ", after taxes.\n");
1099         }
1100         {  // inverted section
1101             auto nodes = compile("{{^repo}}\n  No repos :(\n{{/repo}}\n");
1102             assert(nodes[0].type == NodeType.section);
1103             assert(nodes[0].key  == ["repo"]);
1104             assert(nodes[0].flag == true);
1105 
1106             auto childs = nodes[0].childs;
1107             assert(childs[0].type == NodeType.text);
1108             assert(childs[0].text == "  No repos :(\n");
1109         }
1110         {  // partial and set delimiter
1111             auto nodes = compile("{{=<% %>=}}<%> erb_style %>");
1112             assert(nodes[0].type == NodeType.partial);
1113             assert(nodes[0].key  == ["erb_style"]);
1114         }
1115     }
1116 
1117     private static String[] parseKey(String src, String eTag, out size_t end)
1118     {
1119         String[] key;
1120         size_t index = 0;
1121         size_t keySegmentStart = 0;
1122         // Index from before eating whitespace, so stripRight
1123         // doesn't need to be called on each segment of the key.
1124         size_t beforeEatWSIndex = 0;
1125 
1126         void advance(size_t length)
1127         {
1128             if (index + length >= src.length)
1129                 throw new MustacheException("Mustache tag is not closed");
1130 
1131             index += length;
1132             beforeEatWSIndex = index;
1133         }
1134 
1135         void eatWhitespace()
1136         {
1137             beforeEatWSIndex = index;
1138             index = src.length - src[index..$].stripLeft().length;
1139         }
1140 
1141         void acceptKeySegment()
1142         {
1143             if (keySegmentStart >= beforeEatWSIndex)
1144                 throw new MustacheException("Missing tag name");
1145 
1146             key ~= src[keySegmentStart .. beforeEatWSIndex];
1147         }
1148 
1149         eatWhitespace();
1150         keySegmentStart = index;
1151 
1152         enum String dot = ".";
1153         while (true) {
1154             if (src[index..$].startsWith(eTag)) {
1155                 acceptKeySegment();
1156                 break;
1157             } else if (src[index..$].startsWith(dot)) {
1158                 acceptKeySegment();
1159                 advance(dot.length);
1160                 eatWhitespace();
1161                 keySegmentStart = index;
1162             } else {
1163                 advance(1);
1164                 eatWhitespace();
1165             }
1166         }
1167 
1168         end = index;
1169         return key;
1170     }
1171 
1172     unittest
1173     {
1174         {  // single char, single segment, no whitespace
1175             size_t end;
1176             String src = "a}}";
1177             auto key = parseKey(src, "}}", end);
1178             assert(key.length == 1);
1179             assert(key[0] == "a");
1180             assert(src[end..$] == "}}");
1181         }
1182         {  // multiple chars, single segment, no whitespace
1183             size_t end;
1184             String src = "Mio}}";
1185             auto key = parseKey(src, "}}", end);
1186             assert(key.length == 1);
1187             assert(key[0] == "Mio");
1188             assert(src[end..$] == "}}");
1189         }
1190         {  // single char, multiple segments, no whitespace
1191             size_t end;
1192             String src = "a.b.c}}";
1193             auto key = parseKey(src, "}}", end);
1194             assert(key.length == 3);
1195             assert(key[0] == "a");
1196             assert(key[1] == "b");
1197             assert(key[2] == "c");
1198             assert(src[end..$] == "}}");
1199         }
1200         {  // multiple chars, multiple segments, no whitespace
1201             size_t end;
1202             String src = "Mio.Ritsu.Yui}}";
1203             auto key = parseKey(src, "}}", end);
1204             assert(key.length == 3);
1205             assert(key[0] == "Mio");
1206             assert(key[1] == "Ritsu");
1207             assert(key[2] == "Yui");
1208             assert(src[end..$] == "}}");
1209         }
1210         {  // whitespace
1211             size_t end;
1212             String src = "  Mio  .  Ritsu  }}";
1213             auto key = parseKey(src, "}}", end);
1214             assert(key.length == 2);
1215             assert(key[0] == "Mio");
1216             assert(key[1] == "Ritsu");
1217             assert(src[end..$] == "}}");
1218         }
1219         {  // single char custom end delimiter
1220             size_t end;
1221             String src = "Ritsu-";
1222             auto key = parseKey(src, "-", end);
1223             assert(key.length == 1);
1224             assert(key[0] == "Ritsu");
1225             assert(src[end..$] == "-");
1226         }
1227         {  // extra chars at end
1228             size_t end;
1229             String src = "Ritsu}}abc";
1230             auto key = parseKey(src, "}}", end);
1231             assert(key.length == 1);
1232             assert(key[0] == "Ritsu");
1233             assert(src[end..$] == "}}abc");
1234         }
1235         {  // error: no end delimiter
1236             size_t end;
1237             String src = "a.b.c";
1238             try {
1239                 auto key = parseKey(src, "}}", end);
1240                 assert(false);
1241             } catch (MustacheException e) { }
1242         }
1243         {  // error: missing tag name
1244             size_t end;
1245             String src = "  }}";
1246             try {
1247                 auto key = parseKey(src, "}}", end);
1248                 assert(false);
1249             } catch (MustacheException e) { }
1250         }
1251         {  // error: missing ending tag name
1252             size_t end;
1253             String src = "Mio.}}";
1254             try {
1255                 auto key = parseKey(src, "}}", end);
1256                 assert(false);
1257             } catch (MustacheException e) { }
1258         }
1259         {  // error: missing middle tag name
1260             size_t end;
1261             String src = "Mio. .Ritsu}}";
1262             try {
1263                 auto key = parseKey(src, "}}", end);
1264                 assert(false);
1265             } catch (MustacheException e) { }
1266         }
1267     }
1268 
1269     @trusted
1270     static String keyToString(in String[] key)
1271     {
1272         if (key.length == 0)
1273             return null;
1274 
1275         if (key.length == 1)
1276             return key[0];
1277 
1278         Appender!String buf;
1279         foreach (index, segment; key) {
1280             if (index != 0)
1281                 buf.put('.');
1282 
1283             buf.put(segment);
1284         }
1285 
1286         return buf.data;
1287     }
1288 
1289     /*
1290      * Mustache's node types
1291      */
1292     static enum NodeType
1293     {
1294         text,     /// outside tag
1295         var,      /// {{}} or {{{}}} or {{&}}
1296         section,  /// {{#}} or {{^}}
1297         partial   /// {{>}}
1298     }
1299 
1300 
1301     /*
1302      * Intermediate Representation of Mustache
1303      */
1304     static struct Node
1305     {
1306         NodeType type;
1307 
1308         union
1309         {
1310             String text;
1311 
1312             struct
1313             {
1314                 String[] key;
1315                 bool     flag;    // true is inverted or unescape
1316                 Node[]   childs;  // for list section
1317                 String   source;  // for lambda section
1318             }
1319         }
1320 
1321         @trusted nothrow
1322         {
1323             /**
1324              * Constructs with arguments.
1325              *
1326              * Params:
1327              *   t = raw text
1328              */
1329             this(String t)
1330             {
1331                 type = NodeType.text;
1332                 text = t;
1333             }
1334 
1335             /**
1336              * ditto
1337              *
1338              * Params:
1339              *   t = Mustache's node type
1340              *   k = key string of tag
1341              *   f = invert? or escape?
1342              */
1343             this(NodeType t, String[] k, bool f = false)
1344             {
1345                 type = t;
1346                 key  = k;
1347                 flag = f;
1348             }
1349         }
1350 
1351         /**
1352          * Represents the internal status as a string.
1353          *
1354          * Returns:
1355          *  stringized node representation.
1356          */
1357         string toString() const
1358         {
1359             string result;
1360 
1361             final switch (type) {
1362             case NodeType.text:
1363                 result = "[T : \"" ~ to!string(text) ~ "\"]";
1364                 break;
1365             case NodeType.var:
1366                 result = "[" ~ (flag ? "E" : "V") ~ " : \"" ~ keyToString(key) ~ "\"]";
1367                 break;
1368             case NodeType.section:
1369                 result = "[" ~ (flag ? "I" : "S") ~ " : \"" ~ keyToString(key) ~ "\", [ ";
1370                 foreach (ref node; childs)
1371                     result ~= node.toString() ~ " ";
1372                 result ~= "], \"" ~ to!string(source) ~ "\"]";
1373                 break;
1374             case NodeType.partial:
1375                 result = "[P : \"" ~ keyToString(key) ~ "\"]";
1376                 break;
1377             }
1378 
1379             return result;
1380         }
1381     }
1382 
1383     unittest
1384     {
1385         Node section;
1386         Node[] nodes, childs;
1387 
1388         nodes ~= Node("Hi ");
1389         nodes ~= Node(NodeType.var, ["name"]);
1390         nodes ~= Node(NodeType.partial, ["redbull"]);
1391         {
1392             childs ~= Node("Ritsu is ");
1393             childs ~= Node(NodeType.var, ["attr"], true);
1394             section = Node(NodeType.section, ["ritsu"], false);
1395             section.childs = childs;
1396             nodes ~= section;
1397         }
1398 
1399         assert(to!string(nodes) == `[[T : "Hi "], [V : "name"], [P : "redbull"], ` ~
1400                                    `[S : "ritsu", [ [T : "Ritsu is "] [E : "attr"] ], ""]]`);
1401     }
1402 }
1403 
1404 unittest
1405 {
1406     alias MustacheEngine!(string) Mustache;
1407 
1408     std.file.write("unittest.mustache", "Level: {{lvl}}");
1409     scope(exit) std.file.remove("unittest.mustache");
1410 
1411     Mustache mustache;
1412     auto context = new Mustache.Context;
1413 
1414     { // no
1415         mustache.level = Mustache.CacheLevel.no;
1416         context["lvl"] = "no";
1417         assert(mustache.render("unittest", context) == "Level: no");
1418         assert(mustache.caches_.length == 0);
1419     }
1420     { // check
1421         mustache.level = Mustache.CacheLevel.check;
1422         context["lvl"] = "check";
1423         assert(mustache.render("unittest", context) == "Level: check");
1424         assert(mustache.caches_.length > 0);
1425 
1426         core.thread.Thread.sleep(dur!"seconds"(1));
1427         std.file.write("unittest.mustache", "Modified");
1428         assert(mustache.render("unittest", context) == "Modified");
1429     }
1430     mustache.caches_.remove("./unittest.mustache");  // remove previous cache
1431     { // once
1432         mustache.level = Mustache.CacheLevel.once;
1433         context["lvl"] = "once";
1434         assert(mustache.render("unittest", context) == "Modified");
1435         assert(mustache.caches_.length > 0);
1436 
1437         core.thread.Thread.sleep(dur!"seconds"(1));
1438         std.file.write("unittest.mustache", "Level: {{lvl}}");
1439         assert(mustache.render("unittest", context) == "Modified");
1440     }
1441 }
1442 
1443 unittest
1444 {
1445     alias Mustache = MustacheEngine!(string);
1446 
1447     std.file.write("unittest.mustache", "{{>name}}");
1448     scope(exit) std.file.remove("unittest.mustache");
1449     std.file.write("other.mustache", "Ok");
1450     scope(exit) std.file.remove("other.mustache");
1451 
1452     Mustache mustache;
1453     auto context = new Mustache.Context;
1454     mustache.findPath((path) {
1455         if (path == "name") {
1456             return "other." ~ mustache.ext;
1457         } else {
1458             return path ~ "." ~ mustache.ext;
1459         }
1460     });
1461 
1462     assert(mustache.render("unittest", context) == "Ok");
1463 }