integration/lib/mustache.js
changeset 6 547b3ddedf7f
parent 2 78f71aa0a477
equal deleted inserted replaced
2:78f71aa0a477 6:547b3ddedf7f
       
     1 /*!
       
     2  * mustache.js - Logic-less {{mustache}} templates with JavaScript
       
     3  * http://github.com/janl/mustache.js
       
     4  */
       
     5 
       
     6 /*global define: false*/
       
     7 
       
     8 (function (root, factory) {
       
     9   if (typeof exports === "object" && exports) {
       
    10     factory(exports); // CommonJS
       
    11   } else {
       
    12     var mustache = {};
       
    13     factory(mustache);
       
    14     if (typeof define === "function" && define.amd) {
       
    15       define(mustache); // AMD
       
    16     } else {
       
    17       root.Mustache = mustache; // <script>
       
    18     }
       
    19   }
       
    20 }(this, function (mustache) {
       
    21 
       
    22   var whiteRe = /\s*/;
       
    23   var spaceRe = /\s+/;
       
    24   var nonSpaceRe = /\S/;
       
    25   var eqRe = /\s*=/;
       
    26   var curlyRe = /\s*\}/;
       
    27   var tagRe = /#|\^|\/|>|\{|&|=|!/;
       
    28 
       
    29   // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
       
    30   // See https://github.com/janl/mustache.js/issues/189
       
    31   var RegExp_test = RegExp.prototype.test;
       
    32   function testRegExp(re, string) {
       
    33     return RegExp_test.call(re, string);
       
    34   }
       
    35 
       
    36   function isWhitespace(string) {
       
    37     return !testRegExp(nonSpaceRe, string);
       
    38   }
       
    39 
       
    40   var Object_toString = Object.prototype.toString;
       
    41   var isArray = Array.isArray || function (obj) {
       
    42     return Object_toString.call(obj) === '[object Array]';
       
    43   };
       
    44 
       
    45   function escapeRegExp(string) {
       
    46     return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
       
    47   }
       
    48 
       
    49   var entityMap = {
       
    50     "&": "&amp;",
       
    51     "<": "&lt;",
       
    52     ">": "&gt;",
       
    53     '"': '&quot;',
       
    54     "'": '&#39;',
       
    55     "/": '&#x2F;'
       
    56   };
       
    57 
       
    58   function escapeHtml(string) {
       
    59     return String(string).replace(/[&<>"'\/]/g, function (s) {
       
    60       return entityMap[s];
       
    61     });
       
    62   }
       
    63 
       
    64   function Scanner(string) {
       
    65     this.string = string;
       
    66     this.tail = string;
       
    67     this.pos = 0;
       
    68   }
       
    69 
       
    70   /**
       
    71    * Returns `true` if the tail is empty (end of string).
       
    72    */
       
    73   Scanner.prototype.eos = function () {
       
    74     return this.tail === "";
       
    75   };
       
    76 
       
    77   /**
       
    78    * Tries to match the given regular expression at the current position.
       
    79    * Returns the matched text if it can match, the empty string otherwise.
       
    80    */
       
    81   Scanner.prototype.scan = function (re) {
       
    82     var match = this.tail.match(re);
       
    83 
       
    84     if (match && match.index === 0) {
       
    85       this.tail = this.tail.substring(match[0].length);
       
    86       this.pos += match[0].length;
       
    87       return match[0];
       
    88     }
       
    89 
       
    90     return "";
       
    91   };
       
    92 
       
    93   /**
       
    94    * Skips all text until the given regular expression can be matched. Returns
       
    95    * the skipped string, which is the entire tail if no match can be made.
       
    96    */
       
    97   Scanner.prototype.scanUntil = function (re) {
       
    98     var match, pos = this.tail.search(re);
       
    99 
       
   100     switch (pos) {
       
   101     case -1:
       
   102       match = this.tail;
       
   103       this.pos += this.tail.length;
       
   104       this.tail = "";
       
   105       break;
       
   106     case 0:
       
   107       match = "";
       
   108       break;
       
   109     default:
       
   110       match = this.tail.substring(0, pos);
       
   111       this.tail = this.tail.substring(pos);
       
   112       this.pos += pos;
       
   113     }
       
   114 
       
   115     return match;
       
   116   };
       
   117 
       
   118   function Context(view, parent) {
       
   119     this.view = view || {};
       
   120     this.parent = parent;
       
   121     this._cache = {};
       
   122   }
       
   123 
       
   124   Context.make = function (view) {
       
   125     return (view instanceof Context) ? view : new Context(view);
       
   126   };
       
   127 
       
   128   Context.prototype.push = function (view) {
       
   129     return new Context(view, this);
       
   130   };
       
   131 
       
   132   Context.prototype.lookup = function (name) {
       
   133     var value = this._cache[name];
       
   134 
       
   135     if (!value) {
       
   136       if (name == '.') {
       
   137         value = this.view;
       
   138       } else {
       
   139         var context = this;
       
   140 
       
   141         while (context) {
       
   142           if (name.indexOf('.') > 0) {
       
   143             value = context.view;
       
   144             var names = name.split('.'), i = 0;
       
   145             while (value && i < names.length) {
       
   146               value = value[names[i++]];
       
   147             }
       
   148           } else {
       
   149             value = context.view[name];
       
   150           }
       
   151 
       
   152           if (value != null) break;
       
   153 
       
   154           context = context.parent;
       
   155         }
       
   156       }
       
   157 
       
   158       this._cache[name] = value;
       
   159     }
       
   160 
       
   161     if (typeof value === 'function') value = value.call(this.view);
       
   162 
       
   163     return value;
       
   164   };
       
   165 
       
   166   function Writer() {
       
   167     this.clearCache();
       
   168   }
       
   169 
       
   170   Writer.prototype.clearCache = function () {
       
   171     this._cache = {};
       
   172     this._partialCache = {};
       
   173   };
       
   174 
       
   175   Writer.prototype.compile = function (template, tags) {
       
   176     var fn = this._cache[template];
       
   177 
       
   178     if (!fn) {
       
   179       var tokens = mustache.parse(template, tags);
       
   180       fn = this._cache[template] = this.compileTokens(tokens, template);
       
   181     }
       
   182 
       
   183     return fn;
       
   184   };
       
   185 
       
   186   Writer.prototype.compilePartial = function (name, template, tags) {
       
   187     var fn = this.compile(template, tags);
       
   188     this._partialCache[name] = fn;
       
   189     return fn;
       
   190   };
       
   191 
       
   192   Writer.prototype.getPartial = function (name) {
       
   193     if (!(name in this._partialCache) && this._loadPartial) {
       
   194       this.compilePartial(name, this._loadPartial(name));
       
   195     }
       
   196 
       
   197     return this._partialCache[name];
       
   198   };
       
   199 
       
   200   Writer.prototype.compileTokens = function (tokens, template) {
       
   201     var self = this;
       
   202     return function (view, partials) {
       
   203       if (partials) {
       
   204         if (typeof partials === 'function') {
       
   205           self._loadPartial = partials;
       
   206         } else {
       
   207           for (var name in partials) {
       
   208             self.compilePartial(name, partials[name]);
       
   209           }
       
   210         }
       
   211       }
       
   212 
       
   213       return renderTokens(tokens, self, Context.make(view), template);
       
   214     };
       
   215   };
       
   216 
       
   217   Writer.prototype.render = function (template, view, partials) {
       
   218     return this.compile(template)(view, partials);
       
   219   };
       
   220 
       
   221   /**
       
   222    * Low-level function that renders the given `tokens` using the given `writer`
       
   223    * and `context`. The `template` string is only needed for templates that use
       
   224    * higher-order sections to extract the portion of the original template that
       
   225    * was contained in that section.
       
   226    */
       
   227   function renderTokens(tokens, writer, context, template) {
       
   228     var buffer = '';
       
   229 
       
   230     var token, tokenValue, value;
       
   231     for (var i = 0, len = tokens.length; i < len; ++i) {
       
   232       token = tokens[i];
       
   233       tokenValue = token[1];
       
   234 
       
   235       switch (token[0]) {
       
   236       case '#':
       
   237         value = context.lookup(tokenValue);
       
   238 
       
   239         if (typeof value === 'object') {
       
   240           if (isArray(value)) {
       
   241             for (var j = 0, jlen = value.length; j < jlen; ++j) {
       
   242               buffer += renderTokens(token[4], writer, context.push(value[j]), template);
       
   243             }
       
   244           } else if (value) {
       
   245             buffer += renderTokens(token[4], writer, context.push(value), template);
       
   246           }
       
   247         } else if (typeof value === 'function') {
       
   248           var text = template == null ? null : template.slice(token[3], token[5]);
       
   249           value = value.call(context.view, text, function (template) {
       
   250             return writer.render(template, context);
       
   251           });
       
   252           if (value != null) buffer += value;
       
   253         } else if (value) {
       
   254           buffer += renderTokens(token[4], writer, context, template);
       
   255         }
       
   256 
       
   257         break;
       
   258       case '^':
       
   259         value = context.lookup(tokenValue);
       
   260 
       
   261         // Use JavaScript's definition of falsy. Include empty arrays.
       
   262         // See https://github.com/janl/mustache.js/issues/186
       
   263         if (!value || (isArray(value) && value.length === 0)) {
       
   264           buffer += renderTokens(token[4], writer, context, template);
       
   265         }
       
   266 
       
   267         break;
       
   268       case '>':
       
   269         value = writer.getPartial(tokenValue);
       
   270         if (typeof value === 'function') buffer += value(context);
       
   271         break;
       
   272       case '&':
       
   273         value = context.lookup(tokenValue);
       
   274         if (value != null) buffer += value;
       
   275         break;
       
   276       case 'name':
       
   277         value = context.lookup(tokenValue);
       
   278         if (value != null) buffer += mustache.escape(value);
       
   279         break;
       
   280       case 'text':
       
   281         buffer += tokenValue;
       
   282         break;
       
   283       }
       
   284     }
       
   285 
       
   286     return buffer;
       
   287   }
       
   288 
       
   289   /**
       
   290    * Forms the given array of `tokens` into a nested tree structure where
       
   291    * tokens that represent a section have two additional items: 1) an array of
       
   292    * all tokens that appear in that section and 2) the index in the original
       
   293    * template that represents the end of that section.
       
   294    */
       
   295   function nestTokens(tokens) {
       
   296     var tree = [];
       
   297     var collector = tree;
       
   298     var sections = [];
       
   299 
       
   300     var token;
       
   301     for (var i = 0, len = tokens.length; i < len; ++i) {
       
   302       token = tokens[i];
       
   303       switch (token[0]) {
       
   304       case '#':
       
   305       case '^':
       
   306         sections.push(token);
       
   307         collector.push(token);
       
   308         collector = token[4] = [];
       
   309         break;
       
   310       case '/':
       
   311         var section = sections.pop();
       
   312         section[5] = token[2];
       
   313         collector = sections.length > 0 ? sections[sections.length - 1][4] : tree;
       
   314         break;
       
   315       default:
       
   316         collector.push(token);
       
   317       }
       
   318     }
       
   319 
       
   320     return tree;
       
   321   }
       
   322 
       
   323   /**
       
   324    * Combines the values of consecutive text tokens in the given `tokens` array
       
   325    * to a single token.
       
   326    */
       
   327   function squashTokens(tokens) {
       
   328     var squashedTokens = [];
       
   329 
       
   330     var token, lastToken;
       
   331     for (var i = 0, len = tokens.length; i < len; ++i) {
       
   332       token = tokens[i];
       
   333       if (token) {
       
   334         if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
       
   335           lastToken[1] += token[1];
       
   336           lastToken[3] = token[3];
       
   337         } else {
       
   338           lastToken = token;
       
   339           squashedTokens.push(token);
       
   340         }
       
   341       }
       
   342     }
       
   343 
       
   344     return squashedTokens;
       
   345   }
       
   346 
       
   347   function escapeTags(tags) {
       
   348     return [
       
   349       new RegExp(escapeRegExp(tags[0]) + "\\s*"),
       
   350       new RegExp("\\s*" + escapeRegExp(tags[1]))
       
   351     ];
       
   352   }
       
   353 
       
   354   /**
       
   355    * Breaks up the given `template` string into a tree of token objects. If
       
   356    * `tags` is given here it must be an array with two string values: the
       
   357    * opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
       
   358    * course, the default is to use mustaches (i.e. Mustache.tags).
       
   359    */
       
   360   function parseTemplate(template, tags) {
       
   361     template = template || '';
       
   362     tags = tags || mustache.tags;
       
   363 
       
   364     if (typeof tags === 'string') tags = tags.split(spaceRe);
       
   365     if (tags.length !== 2) throw new Error('Invalid tags: ' + tags.join(', '));
       
   366 
       
   367     var tagRes = escapeTags(tags);
       
   368     var scanner = new Scanner(template);
       
   369 
       
   370     var sections = [];     // Stack to hold section tokens
       
   371     var tokens = [];       // Buffer to hold the tokens
       
   372     var spaces = [];       // Indices of whitespace tokens on the current line
       
   373     var hasTag = false;    // Is there a {{tag}} on the current line?
       
   374     var nonSpace = false;  // Is there a non-space char on the current line?
       
   375 
       
   376     // Strips all whitespace tokens array for the current line
       
   377     // if there was a {{#tag}} on it and otherwise only space.
       
   378     function stripSpace() {
       
   379       if (hasTag && !nonSpace) {
       
   380         while (spaces.length) {
       
   381           delete tokens[spaces.pop()];
       
   382         }
       
   383       } else {
       
   384         spaces = [];
       
   385       }
       
   386 
       
   387       hasTag = false;
       
   388       nonSpace = false;
       
   389     }
       
   390 
       
   391     var start, type, value, chr, token;
       
   392     while (!scanner.eos()) {
       
   393       start = scanner.pos;
       
   394 
       
   395       // Match any text between tags.
       
   396       value = scanner.scanUntil(tagRes[0]);
       
   397       if (value) {
       
   398         for (var i = 0, len = value.length; i < len; ++i) {
       
   399           chr = value.charAt(i);
       
   400 
       
   401           if (isWhitespace(chr)) {
       
   402             spaces.push(tokens.length);
       
   403           } else {
       
   404             nonSpace = true;
       
   405           }
       
   406 
       
   407           tokens.push(['text', chr, start, start + 1]);
       
   408           start += 1;
       
   409 
       
   410           // Check for whitespace on the current line.
       
   411           if (chr == '\n') stripSpace();
       
   412         }
       
   413       }
       
   414 
       
   415       // Match the opening tag.
       
   416       if (!scanner.scan(tagRes[0])) break;
       
   417       hasTag = true;
       
   418 
       
   419       // Get the tag type.
       
   420       type = scanner.scan(tagRe) || 'name';
       
   421       scanner.scan(whiteRe);
       
   422 
       
   423       // Get the tag value.
       
   424       if (type === '=') {
       
   425         value = scanner.scanUntil(eqRe);
       
   426         scanner.scan(eqRe);
       
   427         scanner.scanUntil(tagRes[1]);
       
   428       } else if (type === '{') {
       
   429         value = scanner.scanUntil(new RegExp('\\s*' + escapeRegExp('}' + tags[1])));
       
   430         scanner.scan(curlyRe);
       
   431         scanner.scanUntil(tagRes[1]);
       
   432         type = '&';
       
   433       } else {
       
   434         value = scanner.scanUntil(tagRes[1]);
       
   435       }
       
   436 
       
   437       // Match the closing tag.
       
   438       if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos);
       
   439 
       
   440       token = [type, value, start, scanner.pos];
       
   441       tokens.push(token);
       
   442 
       
   443       if (type === '#' || type === '^') {
       
   444         sections.push(token);
       
   445       } else if (type === '/') {
       
   446         // Check section nesting.
       
   447         if (sections.length === 0) throw new Error('Unopened section "' + value + '" at ' + start);
       
   448         var openSection = sections.pop();
       
   449         if (openSection[1] !== value) throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
       
   450       } else if (type === 'name' || type === '{' || type === '&') {
       
   451         nonSpace = true;
       
   452       } else if (type === '=') {
       
   453         // Set the tags for the next time around.
       
   454         tags = value.split(spaceRe);
       
   455         if (tags.length !== 2) throw new Error('Invalid tags at ' + start + ': ' + tags.join(', '));
       
   456         tagRes = escapeTags(tags);
       
   457       }
       
   458     }
       
   459 
       
   460     // Make sure there are no open sections when we're done.
       
   461     var openSection = sections.pop();
       
   462     if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
       
   463 
       
   464     tokens = squashTokens(tokens);
       
   465 
       
   466     return nestTokens(tokens);
       
   467   }
       
   468 
       
   469   mustache.name = "mustache.js";
       
   470   mustache.version = "0.7.2";
       
   471   mustache.tags = ["{{", "}}"];
       
   472 
       
   473   mustache.Scanner = Scanner;
       
   474   mustache.Context = Context;
       
   475   mustache.Writer = Writer;
       
   476 
       
   477   mustache.parse = parseTemplate;
       
   478 
       
   479   // Export the escaping function so that the user may override it.
       
   480   // See https://github.com/janl/mustache.js/issues/244
       
   481   mustache.escape = escapeHtml;
       
   482 
       
   483   // All Mustache.* functions use this writer.
       
   484   var defaultWriter = new Writer();
       
   485 
       
   486   /**
       
   487    * Clears all cached templates and partials in the default writer.
       
   488    */
       
   489   mustache.clearCache = function () {
       
   490     return defaultWriter.clearCache();
       
   491   };
       
   492 
       
   493   /**
       
   494    * Compiles the given `template` to a reusable function using the default
       
   495    * writer.
       
   496    */
       
   497   mustache.compile = function (template, tags) {
       
   498     return defaultWriter.compile(template, tags);
       
   499   };
       
   500 
       
   501   /**
       
   502    * Compiles the partial with the given `name` and `template` to a reusable
       
   503    * function using the default writer.
       
   504    */
       
   505   mustache.compilePartial = function (name, template, tags) {
       
   506     return defaultWriter.compilePartial(name, template, tags);
       
   507   };
       
   508 
       
   509   /**
       
   510    * Compiles the given array of tokens (the output of a parse) to a reusable
       
   511    * function using the default writer.
       
   512    */
       
   513   mustache.compileTokens = function (tokens, template) {
       
   514     return defaultWriter.compileTokens(tokens, template);
       
   515   };
       
   516 
       
   517   /**
       
   518    * Renders the `template` with the given `view` and `partials` using the
       
   519    * default writer.
       
   520    */
       
   521   mustache.render = function (template, view, partials) {
       
   522     return defaultWriter.render(template, view, partials);
       
   523   };
       
   524 
       
   525   // This is here for backwards compatibility with 0.4.x.
       
   526   mustache.to_html = function (template, view, partials, send) {
       
   527     var result = mustache.render(template, view, partials);
       
   528 
       
   529     if (typeof send === "function") {
       
   530       send(result);
       
   531     } else {
       
   532       return result;
       
   533     }
       
   534   };
       
   535 
       
   536 }));