timeline/lib/mustache.js
changeset 65 03bbfd9cd3c6
equal deleted inserted replaced
64:d1e2781852dc 65:03bbfd9cd3c6
       
     1 /*!
       
     2  * mustache.js - Logic-less {{mustache}} templates with JavaScript
       
     3  * http://github.com/janl/mustache.js
       
     4  */
       
     5 var Mustache = (typeof module !== "undefined" && module.exports) || {};
       
     6 
       
     7 (function (exports) {
       
     8 
       
     9   exports.name = "mustache.js";
       
    10   exports.version = "0.5.0-dev";
       
    11   exports.tags = ["{{", "}}"];
       
    12   exports.parse = parse;
       
    13   exports.compile = compile;
       
    14   exports.render = render;
       
    15   exports.clearCache = clearCache;
       
    16 
       
    17   // This is here for backwards compatibility with 0.4.x.
       
    18   exports.to_html = function (template, view, partials, send) {
       
    19     var result = render(template, view, partials);
       
    20 
       
    21     if (typeof send === "function") {
       
    22       send(result);
       
    23     } else {
       
    24       return result;
       
    25     }
       
    26   };
       
    27 
       
    28   var _toString = Object.prototype.toString;
       
    29   var _isArray = Array.isArray;
       
    30   var _forEach = Array.prototype.forEach;
       
    31   var _trim = String.prototype.trim;
       
    32 
       
    33   var isArray;
       
    34   if (_isArray) {
       
    35     isArray = _isArray;
       
    36   } else {
       
    37     isArray = function (obj) {
       
    38       return _toString.call(obj) === "[object Array]";
       
    39     };
       
    40   }
       
    41 
       
    42   var forEach;
       
    43   if (_forEach) {
       
    44     forEach = function (obj, callback, scope) {
       
    45       return _forEach.call(obj, callback, scope);
       
    46     };
       
    47   } else {
       
    48     forEach = function (obj, callback, scope) {
       
    49       for (var i = 0, len = obj.length; i < len; ++i) {
       
    50         callback.call(scope, obj[i], i, obj);
       
    51       }
       
    52     };
       
    53   }
       
    54 
       
    55   var spaceRe = /^\s*$/;
       
    56 
       
    57   function isWhitespace(string) {
       
    58     return spaceRe.test(string);
       
    59   }
       
    60 
       
    61   var trim;
       
    62   if (_trim) {
       
    63     trim = function (string) {
       
    64       return string == null ? "" : _trim.call(string);
       
    65     };
       
    66   } else {
       
    67     var trimLeft, trimRight;
       
    68 
       
    69     if (isWhitespace("\xA0")) {
       
    70       trimLeft = /^\s+/;
       
    71       trimRight = /\s+$/;
       
    72     } else {
       
    73       // IE doesn't match non-breaking spaces with \s, thanks jQuery.
       
    74       trimLeft = /^[\s\xA0]+/;
       
    75       trimRight = /[\s\xA0]+$/;
       
    76     }
       
    77 
       
    78     trim = function (string) {
       
    79       return string == null ? "" :
       
    80         String(string).replace(trimLeft, "").replace(trimRight, "");
       
    81     };
       
    82   }
       
    83 
       
    84   var escapeMap = {
       
    85     "&": "&amp;",
       
    86     "<": "&lt;",
       
    87     ">": "&gt;",
       
    88     '"': '&quot;',
       
    89     "'": '&#39;'
       
    90   };
       
    91 
       
    92   function escapeHTML(string) {
       
    93     return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) {
       
    94       return escapeMap[s] || s;
       
    95     });
       
    96   }
       
    97 
       
    98   /**
       
    99    * Adds the `template`, `line`, and `file` properties to the given error
       
   100    * object and alters the message to provide more useful debugging information.
       
   101    */
       
   102   function debug(e, template, line, file) {
       
   103     file = file || "<template>";
       
   104 
       
   105     var lines = template.split("\n"),
       
   106         start = Math.max(line - 3, 0),
       
   107         end = Math.min(lines.length, line + 3),
       
   108         context = lines.slice(start, end);
       
   109 
       
   110     var c;
       
   111     for (var i = 0, len = context.length; i < len; ++i) {
       
   112       c = i + start + 1;
       
   113       context[i] = (c === line ? " >> " : "    ") + context[i];
       
   114     }
       
   115 
       
   116     e.template = template;
       
   117     e.line = line;
       
   118     e.file = file;
       
   119     e.message = [file + ":" + line, context.join("\n"), "", e.message].join("\n");
       
   120 
       
   121     return e;
       
   122   }
       
   123 
       
   124   /**
       
   125    * Looks up the value of the given `name` in the given context `stack`.
       
   126    */
       
   127   function lookup(name, stack, defaultValue) {
       
   128     if (name === ".") {
       
   129       return stack[stack.length - 1];
       
   130     }
       
   131 
       
   132     var names = name.split(".");
       
   133     var lastIndex = names.length - 1;
       
   134     var target = names[lastIndex];
       
   135 
       
   136     var value, context, i = stack.length, j, localStack;
       
   137     while (i) {
       
   138       localStack = stack.slice(0);
       
   139       context = stack[--i];
       
   140 
       
   141       j = 0;
       
   142       while (j < lastIndex) {
       
   143         context = context[names[j++]];
       
   144 
       
   145         if (context == null) {
       
   146           break;
       
   147         }
       
   148 
       
   149         localStack.push(context);
       
   150       }
       
   151 
       
   152       if (context && typeof context === "object" && target in context) {
       
   153         value = context[target];
       
   154         break;
       
   155       }
       
   156     }
       
   157 
       
   158     // If the value is a function, call it in the current context.
       
   159     if (typeof value === "function") {
       
   160       value = value.call(localStack[localStack.length - 1]);
       
   161     }
       
   162 
       
   163     if (value == null)  {
       
   164       return defaultValue;
       
   165     }
       
   166 
       
   167     return value;
       
   168   }
       
   169 
       
   170   function renderSection(name, stack, callback, inverted) {
       
   171     var buffer = "";
       
   172     var value =  lookup(name, stack);
       
   173 
       
   174     if (inverted) {
       
   175       // From the spec: inverted sections may render text once based on the
       
   176       // inverse value of the key. That is, they will be rendered if the key
       
   177       // doesn't exist, is false, or is an empty list.
       
   178       if (value == null || value === false || (isArray(value) && value.length === 0)) {
       
   179         buffer += callback();
       
   180       }
       
   181     } else if (isArray(value)) {
       
   182       forEach(value, function (value) {
       
   183         stack.push(value);
       
   184         buffer += callback();
       
   185         stack.pop();
       
   186       });
       
   187     } else if (typeof value === "object") {
       
   188       stack.push(value);
       
   189       buffer += callback();
       
   190       stack.pop();
       
   191     } else if (typeof value === "function") {
       
   192       var scope = stack[stack.length - 1];
       
   193       var scopedRender = function (template) {
       
   194         return render(template, scope);
       
   195       };
       
   196       buffer += value.call(scope, callback(), scopedRender) || "";
       
   197     } else if (value) {
       
   198       buffer += callback();
       
   199     }
       
   200 
       
   201     return buffer;
       
   202   }
       
   203 
       
   204   /**
       
   205    * Parses the given `template` and returns the source of a function that,
       
   206    * with the proper arguments, will render the template. Recognized options
       
   207    * include the following:
       
   208    *
       
   209    *   - file     The name of the file the template comes from (displayed in
       
   210    *              error messages)
       
   211    *   - tags     An array of open and close tags the `template` uses. Defaults
       
   212    *              to the value of Mustache.tags
       
   213    *   - debug    Set `true` to log the body of the generated function to the
       
   214    *              console
       
   215    *   - space    Set `true` to preserve whitespace from lines that otherwise
       
   216    *              contain only a {{tag}}. Defaults to `false`
       
   217    */
       
   218   function parse(template, options) {
       
   219     options = options || {};
       
   220 
       
   221     var tags = options.tags || exports.tags,
       
   222         openTag = tags[0],
       
   223         closeTag = tags[tags.length - 1];
       
   224 
       
   225     var code = [
       
   226       'var buffer = "";', // output buffer
       
   227       "\nvar line = 1;", // keep track of source line number
       
   228       "\ntry {",
       
   229       '\nbuffer += "'
       
   230     ];
       
   231 
       
   232     var spaces = [],      // indices of whitespace in code on the current line
       
   233         hasTag = false,   // is there a {{tag}} on the current line?
       
   234         nonSpace = false; // is there a non-space char on the current line?
       
   235 
       
   236     // Strips all space characters from the code array for the current line
       
   237     // if there was a {{tag}} on it and otherwise only spaces.
       
   238     var stripSpace = function () {
       
   239       if (hasTag && !nonSpace && !options.space) {
       
   240         while (spaces.length) {
       
   241           code.splice(spaces.pop(), 1);
       
   242         }
       
   243       } else {
       
   244         spaces = [];
       
   245       }
       
   246 
       
   247       hasTag = false;
       
   248       nonSpace = false;
       
   249     };
       
   250 
       
   251     var sectionStack = [], updateLine, nextOpenTag, nextCloseTag;
       
   252 
       
   253     var setTags = function (source) {
       
   254       tags = trim(source).split(/\s+/);
       
   255       nextOpenTag = tags[0];
       
   256       nextCloseTag = tags[tags.length - 1];
       
   257     };
       
   258 
       
   259     var includePartial = function (source) {
       
   260       code.push(
       
   261         '";',
       
   262         updateLine,
       
   263         '\nvar partial = partials["' + trim(source) + '"];',
       
   264         '\nif (partial) {',
       
   265         '\n  buffer += render(partial,stack[stack.length - 1],partials);',
       
   266         '\n}',
       
   267         '\nbuffer += "'
       
   268       );
       
   269     };
       
   270 
       
   271     var openSection = function (source, inverted) {
       
   272       var name = trim(source);
       
   273 
       
   274       if (name === "") {
       
   275         throw debug(new Error("Section name may not be empty"), template, line, options.file);
       
   276       }
       
   277 
       
   278       sectionStack.push({name: name, inverted: inverted});
       
   279 
       
   280       code.push(
       
   281         '";',
       
   282         updateLine,
       
   283         '\nvar name = "' + name + '";',
       
   284         '\nvar callback = (function () {',
       
   285         '\n  return function () {',
       
   286         '\n    var buffer = "";',
       
   287         '\nbuffer += "'
       
   288       );
       
   289     };
       
   290 
       
   291     var openInvertedSection = function (source) {
       
   292       openSection(source, true);
       
   293     };
       
   294 
       
   295     var closeSection = function (source) {
       
   296       var name = trim(source);
       
   297       var openName = sectionStack.length != 0 && sectionStack[sectionStack.length - 1].name;
       
   298 
       
   299       if (!openName || name != openName) {
       
   300         throw debug(new Error('Section named "' + name + '" was never opened'), template, line, options.file);
       
   301       }
       
   302 
       
   303       var section = sectionStack.pop();
       
   304 
       
   305       code.push(
       
   306         '";',
       
   307         '\n    return buffer;',
       
   308         '\n  };',
       
   309         '\n})();'
       
   310       );
       
   311 
       
   312       if (section.inverted) {
       
   313         code.push("\nbuffer += renderSection(name,stack,callback,true);");
       
   314       } else {
       
   315         code.push("\nbuffer += renderSection(name,stack,callback);");
       
   316       }
       
   317 
       
   318       code.push('\nbuffer += "');
       
   319     };
       
   320 
       
   321     var sendPlain = function (source) {
       
   322       code.push(
       
   323         '";',
       
   324         updateLine,
       
   325         '\nbuffer += lookup("' + trim(source) + '",stack,"");',
       
   326         '\nbuffer += "'
       
   327       );
       
   328     };
       
   329 
       
   330     var sendEscaped = function (source) {
       
   331       code.push(
       
   332         '";',
       
   333         updateLine,
       
   334         '\nbuffer += escapeHTML(lookup("' + trim(source) + '",stack,""));',
       
   335         '\nbuffer += "'
       
   336       );
       
   337     };
       
   338 
       
   339     var line = 1, c, callback;
       
   340     for (var i = 0, len = template.length; i < len; ++i) {
       
   341       if (template.slice(i, i + openTag.length) === openTag) {
       
   342         i += openTag.length;
       
   343         c = template.substr(i, 1);
       
   344         updateLine = '\nline = ' + line + ';';
       
   345         nextOpenTag = openTag;
       
   346         nextCloseTag = closeTag;
       
   347         hasTag = true;
       
   348 
       
   349         switch (c) {
       
   350         case "!": // comment
       
   351           i++;
       
   352           callback = null;
       
   353           break;
       
   354         case "=": // change open/close tags, e.g. {{=<% %>=}}
       
   355           i++;
       
   356           closeTag = "=" + closeTag;
       
   357           callback = setTags;
       
   358           break;
       
   359         case ">": // include partial
       
   360           i++;
       
   361           callback = includePartial;
       
   362           break;
       
   363         case "#": // start section
       
   364           i++;
       
   365           callback = openSection;
       
   366           break;
       
   367         case "^": // start inverted section
       
   368           i++;
       
   369           callback = openInvertedSection;
       
   370           break;
       
   371         case "/": // end section
       
   372           i++;
       
   373           callback = closeSection;
       
   374           break;
       
   375         case "{": // plain variable
       
   376           closeTag = "}" + closeTag;
       
   377           // fall through
       
   378         case "&": // plain variable
       
   379           i++;
       
   380           nonSpace = true;
       
   381           callback = sendPlain;
       
   382           break;
       
   383         default: // escaped variable
       
   384           nonSpace = true;
       
   385           callback = sendEscaped;
       
   386         }
       
   387 
       
   388         var end = template.indexOf(closeTag, i);
       
   389 
       
   390         if (end === -1) {
       
   391           throw debug(new Error('Tag "' + openTag + '" was not closed properly'), template, line, options.file);
       
   392         }
       
   393 
       
   394         var source = template.substring(i, end);
       
   395 
       
   396         if (callback) {
       
   397           callback(source);
       
   398         }
       
   399 
       
   400         // Maintain line count for \n in source.
       
   401         var n = 0;
       
   402         while (~(n = source.indexOf("\n", n))) {
       
   403           line++;
       
   404           n++;
       
   405         }
       
   406 
       
   407         i = end + closeTag.length - 1;
       
   408         openTag = nextOpenTag;
       
   409         closeTag = nextCloseTag;
       
   410       } else {
       
   411         c = template.substr(i, 1);
       
   412 
       
   413         switch (c) {
       
   414         case '"':
       
   415         case "\\":
       
   416           nonSpace = true;
       
   417           code.push("\\" + c);
       
   418           break;
       
   419         case "\r":
       
   420           // Ignore carriage returns.
       
   421           break;
       
   422         case "\n":
       
   423           spaces.push(code.length);
       
   424           code.push("\\n");
       
   425           stripSpace(); // Check for whitespace on the current line.
       
   426           line++;
       
   427           break;
       
   428         default:
       
   429           if (isWhitespace(c)) {
       
   430             spaces.push(code.length);
       
   431           } else {
       
   432             nonSpace = true;
       
   433           }
       
   434 
       
   435           code.push(c);
       
   436         }
       
   437       }
       
   438     }
       
   439 
       
   440     if (sectionStack.length != 0) {
       
   441       throw debug(new Error('Section "' + sectionStack[sectionStack.length - 1].name + '" was not closed properly'), template, line, options.file);
       
   442     }
       
   443 
       
   444     // Clean up any whitespace from a closing {{tag}} that was at the end
       
   445     // of the template without a trailing \n.
       
   446     stripSpace();
       
   447 
       
   448     code.push(
       
   449       '";',
       
   450       "\nreturn buffer;",
       
   451       "\n} catch (e) { throw {error: e, line: line}; }"
       
   452     );
       
   453 
       
   454     // Ignore `buffer += "";` statements.
       
   455     var body = code.join("").replace(/buffer \+= "";\n/g, "");
       
   456 
       
   457     if (options.debug) {
       
   458       if (typeof console != "undefined" && console.log) {
       
   459         console.log(body);
       
   460       } else if (typeof print === "function") {
       
   461         print(body);
       
   462       }
       
   463     }
       
   464 
       
   465     return body;
       
   466   }
       
   467 
       
   468   /**
       
   469    * Used by `compile` to generate a reusable function for the given `template`.
       
   470    */
       
   471   function _compile(template, options) {
       
   472     var args = "view,partials,stack,lookup,escapeHTML,renderSection,render";
       
   473     var body = parse(template, options);
       
   474     var fn = new Function(args, body);
       
   475 
       
   476     // This anonymous function wraps the generated function so we can do
       
   477     // argument coercion, setup some variables, and handle any errors
       
   478     // encountered while executing it.
       
   479     return function (view, partials) {
       
   480       partials = partials || {};
       
   481 
       
   482       var stack = [view]; // context stack
       
   483 
       
   484       try {
       
   485         return fn(view, partials, stack, lookup, escapeHTML, renderSection, render);
       
   486       } catch (e) {
       
   487         throw debug(e.error, template, e.line, options.file);
       
   488       }
       
   489     };
       
   490   }
       
   491 
       
   492   // Cache of pre-compiled templates.
       
   493   var _cache = {};
       
   494 
       
   495   /**
       
   496    * Clear the cache of compiled templates.
       
   497    */
       
   498   function clearCache() {
       
   499     _cache = {};
       
   500   }
       
   501 
       
   502   /**
       
   503    * Compiles the given `template` into a reusable function using the given
       
   504    * `options`. In addition to the options accepted by Mustache.parse,
       
   505    * recognized options include the following:
       
   506    *
       
   507    *   - cache    Set `false` to bypass any pre-compiled version of the given
       
   508    *              template. Otherwise, a given `template` string will be cached
       
   509    *              the first time it is parsed
       
   510    */
       
   511   function compile(template, options) {
       
   512     options = options || {};
       
   513 
       
   514     // Use a pre-compiled version from the cache if we have one.
       
   515     if (options.cache !== false) {
       
   516       if (!_cache[template]) {
       
   517         _cache[template] = _compile(template, options);
       
   518       }
       
   519 
       
   520       return _cache[template];
       
   521     }
       
   522 
       
   523     return _compile(template, options);
       
   524   }
       
   525 
       
   526   /**
       
   527    * High-level function that renders the given `template` using the given
       
   528    * `view` and `partials`. If you need to use any of the template options (see
       
   529    * `compile` above), you must compile in a separate step, and then call that
       
   530    * compiled function.
       
   531    */
       
   532   function render(template, view, partials) {
       
   533     return compile(template)(view, partials);
       
   534   }
       
   535 
       
   536 })(Mustache);