src/js/mustache.js
changeset 39 4b8c52e11a13
equal deleted inserted replaced
38:94899a565089 39:4b8c52e11a13
       
     1 /*
       
     2   mustache.js — Logic-less templates in JavaScript
       
     3 
       
     4   See http://mustache.github.com/ for more info.
       
     5 */
       
     6 
       
     7 var Mustache = function() {
       
     8   var Renderer = function() {};
       
     9 
       
    10   Renderer.prototype = {
       
    11     otag: "{{",
       
    12     ctag: "}}",
       
    13     pragmas: {},
       
    14     buffer: [],
       
    15     pragmas_implemented: {
       
    16       "IMPLICIT-ITERATOR": true
       
    17     },
       
    18     context: {},
       
    19 
       
    20     render: function(template, context, partials, in_recursion) {
       
    21       // reset buffer & set context
       
    22       if(!in_recursion) {
       
    23         this.context = context;
       
    24         this.buffer = []; // TODO: make this non-lazy
       
    25       }
       
    26 
       
    27       // fail fast
       
    28       if(!this.includes("", template)) {
       
    29         if(in_recursion) {
       
    30           return template;
       
    31         } else {
       
    32           this.send(template);
       
    33           return;
       
    34         }
       
    35       }
       
    36 
       
    37       template = this.render_pragmas(template);
       
    38       var html = this.render_section(template, context, partials);
       
    39       if(in_recursion) {
       
    40         return this.render_tags(html, context, partials, in_recursion);
       
    41       }
       
    42 
       
    43       this.render_tags(html, context, partials, in_recursion);
       
    44     },
       
    45 
       
    46     /*
       
    47       Sends parsed lines
       
    48     */
       
    49     send: function(line) {
       
    50       if(line !== "") {
       
    51         this.buffer.push(line);
       
    52       }
       
    53     },
       
    54 
       
    55     /*
       
    56       Looks for %PRAGMAS
       
    57     */
       
    58     render_pragmas: function(template) {
       
    59       // no pragmas
       
    60       if(!this.includes("%", template)) {
       
    61         return template;
       
    62       }
       
    63 
       
    64       var that = this;
       
    65       var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
       
    66             this.ctag, "g");
       
    67       return template.replace(regex, function(match, pragma, options) {
       
    68         if(!that.pragmas_implemented[pragma]) {
       
    69           throw({message: 
       
    70             "This implementation of mustache doesn't understand the '" +
       
    71             pragma + "' pragma"});
       
    72         }
       
    73         that.pragmas[pragma] = {};
       
    74         if(options) {
       
    75           var opts = options.split("=");
       
    76           that.pragmas[pragma][opts[0]] = opts[1];
       
    77         }
       
    78         return "";
       
    79         // ignore unknown pragmas silently
       
    80       });
       
    81     },
       
    82 
       
    83     /*
       
    84       Tries to find a partial in the curent scope and render it
       
    85     */
       
    86     render_partial: function(name, context, partials) {
       
    87       name = this.trim(name);
       
    88       if(!partials || partials[name] === undefined) {
       
    89         throw({message: "unknown_partial '" + name + "'"});
       
    90       }
       
    91       if(typeof(context[name]) != "object") {
       
    92         return this.render(partials[name], context, partials, true);
       
    93       }
       
    94       return this.render(partials[name], context[name], partials, true);
       
    95     },
       
    96 
       
    97     /*
       
    98       Renders inverted (^) and normal (#) sections
       
    99     */
       
   100     render_section: function(template, context, partials) {
       
   101       if(!this.includes("#", template) && !this.includes("^", template)) {
       
   102         return template;
       
   103       }
       
   104 
       
   105       var that = this;
       
   106       // CSW - Added "+?" so it finds the tighest bound, not the widest
       
   107       var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
       
   108               "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
       
   109               "\\s*", "mg");
       
   110 
       
   111       // for each {{#foo}}{{/foo}} section do...
       
   112       return template.replace(regex, function(match, type, name, content) {
       
   113         var value = that.find(name, context);
       
   114         if(type == "^") { // inverted section
       
   115           if(!value || that.is_array(value) && value.length === 0) {
       
   116             // false or empty list, render it
       
   117             return that.render(content, context, partials, true);
       
   118           } else {
       
   119             return "";
       
   120           }
       
   121         } else if(type == "#") { // normal section
       
   122           if(that.is_array(value)) { // Enumerable, Let's loop!
       
   123             return that.map(value, function(row) {
       
   124               return that.render(content, that.create_context(row),
       
   125                 partials, true);
       
   126             }).join("");
       
   127           } else if(that.is_object(value)) { // Object, Use it as subcontext!
       
   128             return that.render(content, that.create_context(value),
       
   129               partials, true);
       
   130           } else if(typeof value === "function") {
       
   131             // higher order section
       
   132             return value.call(context, content, function(text) {
       
   133               return that.render(text, context, partials, true);
       
   134             });
       
   135           } else if(value) { // boolean section
       
   136             return that.render(content, context, partials, true);
       
   137           } else {
       
   138             return "";
       
   139           }
       
   140         }
       
   141       });
       
   142     },
       
   143 
       
   144     /*
       
   145       Replace {{foo}} and friends with values from our view
       
   146     */
       
   147     render_tags: function(template, context, partials, in_recursion) {
       
   148       // tit for tat
       
   149       var that = this;
       
   150 
       
   151       var new_regex = function() {
       
   152         return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
       
   153           that.ctag + "+", "g");
       
   154       };
       
   155 
       
   156       var regex = new_regex();
       
   157       var tag_replace_callback = function(match, operator, name) {
       
   158         switch(operator) {
       
   159         case "!": // ignore comments
       
   160           return "";
       
   161         case "=": // set new delimiters, rebuild the replace regexp
       
   162           that.set_delimiters(name);
       
   163           regex = new_regex();
       
   164           return "";
       
   165         case ">": // render partial
       
   166           return that.render_partial(name, context, partials);
       
   167         case "{": // the triple mustache is unescaped
       
   168           return that.find(name, context);
       
   169         default: // escape the value
       
   170           return that.escape(that.find(name, context));
       
   171         }
       
   172       };
       
   173       var lines = template.split("\n");
       
   174       for(var i = 0; i < lines.length; i++) {
       
   175         lines[i] = lines[i].replace(regex, tag_replace_callback, this);
       
   176         if(!in_recursion) {
       
   177           this.send(lines[i]);
       
   178         }
       
   179       }
       
   180 
       
   181       if(in_recursion) {
       
   182         return lines.join("\n");
       
   183       }
       
   184     },
       
   185 
       
   186     set_delimiters: function(delimiters) {
       
   187       var dels = delimiters.split(" ");
       
   188       this.otag = this.escape_regex(dels[0]);
       
   189       this.ctag = this.escape_regex(dels[1]);
       
   190     },
       
   191 
       
   192     escape_regex: function(text) {
       
   193       // thank you Simon Willison
       
   194       if(!arguments.callee.sRE) {
       
   195         var specials = [
       
   196           '/', '.', '*', '+', '?', '|',
       
   197           '(', ')', '[', ']', '{', '}', '\\'
       
   198         ];
       
   199         arguments.callee.sRE = new RegExp(
       
   200           '(\\' + specials.join('|\\') + ')', 'g'
       
   201         );
       
   202       }
       
   203       return text.replace(arguments.callee.sRE, '\\$1');
       
   204     },
       
   205 
       
   206     /*
       
   207       find `name` in current `context`. That is find me a value
       
   208       from the view object
       
   209     */
       
   210     find: function(name, context) {
       
   211       name = this.trim(name);
       
   212 
       
   213       // Checks whether a value is thruthy or false or 0
       
   214       function is_kinda_truthy(bool) {
       
   215         return bool === false || bool === 0 || bool;
       
   216       }
       
   217 
       
   218       var value;
       
   219       if(is_kinda_truthy(context[name])) {
       
   220         value = context[name];
       
   221       } else if(is_kinda_truthy(this.context[name])) {
       
   222         value = this.context[name];
       
   223       }
       
   224 
       
   225       if(typeof value === "function") {
       
   226         return value.apply(context);
       
   227       }
       
   228       if(value !== undefined) {
       
   229         return value;
       
   230       }
       
   231       // silently ignore unkown variables
       
   232       return "";
       
   233     },
       
   234 
       
   235     // Utility methods
       
   236 
       
   237     /* includes tag */
       
   238     includes: function(needle, haystack) {
       
   239       return haystack.indexOf(this.otag + needle) != -1;
       
   240     },
       
   241 
       
   242     /*
       
   243       Does away with nasty characters
       
   244     */
       
   245     escape: function(s) {
       
   246       s = String(s === null ? "" : s);
       
   247       return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
       
   248         switch(s) {
       
   249         case "&": return "&amp;";
       
   250         case "\\": return "\\\\";
       
   251         case '"': return '&quot;';
       
   252         case "'": return '&#39;';
       
   253         case "<": return "&lt;";
       
   254         case ">": return "&gt;";
       
   255         default: return s;
       
   256         }
       
   257       });
       
   258     },
       
   259 
       
   260     // by @langalex, support for arrays of strings
       
   261     create_context: function(_context) {
       
   262       if(this.is_object(_context)) {
       
   263         return _context;
       
   264       } else {
       
   265         var iterator = ".";
       
   266         if(this.pragmas["IMPLICIT-ITERATOR"]) {
       
   267           iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
       
   268         }
       
   269         var ctx = {};
       
   270         ctx[iterator] = _context;
       
   271         return ctx;
       
   272       }
       
   273     },
       
   274 
       
   275     is_object: function(a) {
       
   276       return a && typeof a == "object";
       
   277     },
       
   278 
       
   279     is_array: function(a) {
       
   280       return Object.prototype.toString.call(a) === '[object Array]';
       
   281     },
       
   282 
       
   283     /*
       
   284       Gets rid of leading and trailing whitespace
       
   285     */
       
   286     trim: function(s) {
       
   287       return s.replace(/^\s*|\s*$/g, "");
       
   288     },
       
   289 
       
   290     /*
       
   291       Why, why, why? Because IE. Cry, cry cry.
       
   292     */
       
   293     map: function(array, fn) {
       
   294       if (typeof array.map == "function") {
       
   295         return array.map(fn);
       
   296       } else {
       
   297         var r = [];
       
   298         var l = array.length;
       
   299         for(var i = 0; i < l; i++) {
       
   300           r.push(fn(array[i]));
       
   301         }
       
   302         return r;
       
   303       }
       
   304     }
       
   305   };
       
   306 
       
   307   return({
       
   308     name: "mustache.js",
       
   309     version: "0.3.1-dev",
       
   310 
       
   311     /*
       
   312       Turns a template and view into HTML
       
   313     */
       
   314     to_html: function(template, view, partials, send_fun) {
       
   315       var renderer = new Renderer();
       
   316       if(send_fun) {
       
   317         renderer.send = send_fun;
       
   318       }
       
   319       renderer.render(template, view, partials);
       
   320       if(!send_fun) {
       
   321         return renderer.buffer.join("\n");
       
   322       }
       
   323     }
       
   324   });
       
   325 }();