source: appstream-generator/contrib/subprojects/mustache-d-0.1.3/src/mustache.d @ 4841

Last change on this file since 4841 was 4841, checked in by Juanma, 2 years ago

Initial release

File size: 43.4 KB
Line 
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 */
10module mustache;
11
12import std.algorithm : all;
13import std.array;    // empty, back, popBack, appender
14import std.conv;     // to
15import std.datetime; // SysTime (I think std.file should import std.datetime as public)
16import std.file;     // read, timeLastModified
17import std.path;     // buildPath
18import std.range;    // isOutputRange
19import std.string;   // strip, chomp, stripLeft
20import std.traits;   // isSomeString, isAssociativeArray
21
22static import std.ascii; // isWhite;
23
24version(unittest) import core.thread;
25
26
27/**
28 * Exception for Mustache
29 */
30class 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 */
73struct 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(in 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 (const 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 (const 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 (const 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 (const 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 (const 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 (const 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
1404unittest
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
1443unittest
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}
Note: See TracBrowser for help on using the repository browser.