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 = "&"; break; 719 case '"': temp = """; break; 720 case '<': temp = "<"; break; 721 case '>': temp = ">"; 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 & 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 }