JavaScript template language shootoff (v920)

Revision 920 of this benchmark created by Brandon Papworth on


Description

A brief comparison of some JavaScript templating engines on a short template: 6 header tags, and 10 list items.

Note: When adding a new test, please ensure that your test returns the same HTML string (or equivalent DOM fragment) as the others.

Preparation HTML

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>

<script src="http://documentcloud.github.com/underscore/underscore.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/0.7.2/mustache.min.js"></script>

<!-- Milk.js -->
<script>
  (function() {
  var Expand, Find, Milk, Parse, TemplateCache, key;
  var __slice = Array.prototype.slice;
  TemplateCache = {};
  Find = function(name, stack, value) {
    var ctx, i, part, parts, _i, _len, _ref, _ref2, _ref3;
    if (value == null) {
      value = null;
    }
    if (name === '.') {
      return stack[stack.length - 1];
    }
    _ref = name.split(/\./), name = _ref[0], parts = 2 <= _ref.length ? __slice.call(_ref, 1) : [];
    for (i = _ref2 = stack.length - 1, _ref3 = -1; (_ref2 <= _ref3 ? i < _ref3 : i > _ref3); (_ref2 <= _ref3 ? i += 1 : i -= 1)) {
      if (stack[i] == null) {
        continue;
      }
      if (!(typeof stack[i] === 'object' && name in (ctx = stack[i]))) {
        continue;
      }
      value = ctx[name];
      break;
    }
    for (_i = 0, _len = parts.length; _i < _len; _i++) {
      part = parts[_i];
      value = Find(part, [value]);
    }
    if (value instanceof Function) {
      value = (function(value) {
        return function() {
          var val;
          val = value.apply(ctx, arguments);
          return (val instanceof Function) && val.apply(null, arguments) || val;
        };
      })(value);
    }
    return value;
  };
  Expand = function() {
    var args, f, obj, tmpl;
    obj = arguments[0], tmpl = arguments[1], args = 3 <= arguments.length ? __slice.call(arguments, 2) : [];
    return ((function() {
      var _i, _len, _results;
      _results = [];
      for (_i = 0, _len = tmpl.length; _i < _len; _i++) {
        f = tmpl[_i];
        _results.push(f.call.apply(f, [obj].concat(__slice.call(args))));
      }
      return _results;
    })()).join('');
  };
  Parse = function(template, delimiters, section) {
    var BuildRegex, buffer, buildInterpolationTag, buildInvertedSectionTag, buildPartialTag, buildSectionTag, cache, content, contentEnd, d, error, escape, isStandalone, match, name, parseError, pos, sectionInfo, tag, tagPattern, tmpl, type, whitespace, _name, _ref, _ref2, _ref3;
    if (delimiters == null) {
      delimiters = ['{{', '}}'];
    }
    if (section == null) {
      section = null;
    }
    cache = (TemplateCache[_name = delimiters.join(' ')] || (TemplateCache[_name] = {}));
    if (template in cache) {
      return cache[template];
    }
    buffer = [];
    BuildRegex = function() {
      var tagClose, tagOpen;
      tagOpen = delimiters[0], tagClose = delimiters[1];
      return RegExp("([\\s\\S]*?)([" + ' ' + "\\t]*)(?:" + tagOpen + "\\s*(?:(!)\\s*([\\s\\S]+?)|(=)\\s*([\\s\\S]+?)\\s*=|({)\\s*(\\w[\\S]*?)\\s*}|([^0-9a-zA-Z._!={]?)\\s*([\\w.][\\S]*?))\\s*" + tagClose + ")", "gm");
    };
    tagPattern = BuildRegex();
    tagPattern.lastIndex = pos = (section || {
      start: 0
    }).start;
    parseError = function(pos, msg) {
      var carets, e, endOfLine, error, indent, key, lastLine, lastTag, lineNo, parsedLines, tagStart;
      (endOfLine = /$/gm).lastIndex = pos;
      endOfLine.exec(template);
      parsedLines = template.substr(0, pos).split('\n');
      lineNo = parsedLines.length;
      lastLine = parsedLines[lineNo - 1];
      tagStart = contentEnd + whitespace.length;
      lastTag = template.substr(tagStart + 1, pos - tagStart - 1);
      indent = new Array(lastLine.length - lastTag.length + 1).join(' ');
      carets = new Array(lastTag.length + 1).join('^');
      lastLine = lastLine + template.substr(pos, endOfLine.lastIndex - pos);
      error = new Error();
      for (key in e = {
        "message": "" + msg + "\n\nLine " + lineNo + ":\n" + lastLine + "\n" + indent + carets,
        "error": msg,
        "line": lineNo,
        "char": indent.length,
        "tag": lastTag
      }) {
        error[key] = e[key];
      }
      return error;
    };
    while (match = tagPattern.exec(template)) {
      _ref = match.slice(1, 3), content = _ref[0], whitespace = _ref[1];
      type = match[3] || match[5] || match[7] || match[9];
      tag = match[4] || match[6] || match[8] || match[10];
      contentEnd = (pos + content.length) - 1;
      pos = tagPattern.lastIndex;
      isStandalone = (contentEnd === -1 || template.charAt(contentEnd) === '\n') && ((_ref2 = template.charAt(pos)) === void 0 || _ref2 === '' || _ref2 === '\r' || _ref2 === '\n');
      if (content) {
        buffer.push((function(content) {
          return function() {
            return content;
          };
        })(content));
      }
      if (isStandalone && (type !== '' && type !== '&' && type !== '{')) {
        if (template.charAt(pos) === '\r') {
          pos += 1;
        }
        if (template.charAt(pos) === '\n') {
          pos += 1;
        }
      } else if (whitespace) {
        buffer.push((function(whitespace) {
          return function() {
            return whitespace;
          };
        })(whitespace));
        contentEnd += whitespace.length;
        whitespace = '';
      }
      switch (type) {
        case '!':
          break;
        case '':
        case '&':
        case '{':
          buildInterpolationTag = function(name, is_unescaped) {
            return function(context) {
              var value, _ref;
              if ((value = (_ref = Find(name, context)) != null ? _ref : '') instanceof Function) {
                value = Expand.apply(null, [this, Parse("" + (value()))].concat(__slice.call(arguments)));
              }
              if (!is_unescaped) {
                value = this.escape("" + value);
              }
              return "" + value;
            };
          };
          buffer.push(buildInterpolationTag(tag, type));
          break;
        case '>':
          buildPartialTag = function(name, indentation) {
            return function(context, partials) {
              var partial;
              partial = partials(name).toString();
              if (indentation) {
                partial = partial.replace(/^(?=.)/gm, indentation);
              }
              return Expand.apply(null, [this, Parse(partial)].concat(__slice.call(arguments)));
            };
          };
          buffer.push(buildPartialTag(tag, whitespace));
          break;
        case '#':
        case '^':
          sectionInfo = {
            name: tag,
            start: pos,
            error: parseError(tagPattern.lastIndex, "Unclosed section '" + tag + "'!")
          };
          _ref3 = Parse(template, delimiters, sectionInfo), tmpl = _ref3[0], pos = _ref3[1];
          sectionInfo['#'] = buildSectionTag = function(name, delims, raw) {
            return function(context) {
              var parsed, result, v, value;
              value = Find(name, context) || [];
              tmpl = value instanceof Function ? value(raw) : raw;
              if (!(value instanceof Array)) {
                value = [value];
              }
              parsed = Parse(tmpl || '', delims);
              context.push(value);
              result = (function() {
                var _i, _len, _results;
                _results = [];
                for (_i = 0, _len = value.length; _i < _len; _i++) {
                  v = value[_i];
                  context[context.length - 1] = v;
                  _results.push(Expand.apply(null, [this, parsed].concat(__slice.call(arguments))));
                }
                return _results;
              }).apply(this, arguments);
              context.pop();
              return result.join('');
            };
          };
          sectionInfo['^'] = buildInvertedSectionTag = function(name, delims, raw) {
            return function(context) {
              var value;
              value = Find(name, context) || [];
              if (!(value instanceof Array)) {
                value = [1];
              }
              value = value.length === 0 ? Parse(raw, delims) : [];
              return Expand.apply(null, [this, value].concat(__slice.call(arguments)));
            };
          };
          buffer.push(sectionInfo[type](tag, delimiters, tmpl));
          break;
        case '/':
          if (section == null) {
            error = "End Section tag '" + tag + "' found, but not in section!";
          } else if (tag !== (name = section.name)) {
            error = "End Section tag closes '" + tag + "'; expected '" + name + "'!";
          }
          if (error) {
            throw parseError(tagPattern.lastIndex, error);
          }
          template = template.slice(section.start, (contentEnd + 1) || 9e9);
          cache[template] = buffer;
          return [template, pos];
        case '=':
          if ((delimiters = tag.split(/\s+/)).length !== 2) {
            error = "Set Delimiters tags should have two and only two values!";
          }
          if (error) {
            throw parseError(tagPattern.lastIndex, error);
          }
          escape = /[-[\]{}()*+?.,\\^$|#]/g;
          delimiters = (function() {
            var _i, _len, _results;
            _results = [];
            for (_i = 0, _len = delimiters.length; _i < _len; _i++) {
              d = delimiters[_i];
              _results.push(d.replace(escape, "\\$&"));
            }
            return _results;
          })();
          tagPattern = BuildRegex();
          break;
        default:
          throw parseError(tagPattern.lastIndex, "Unknown tag type -- " + type);
      }
      tagPattern.lastIndex = pos != null ? pos : template.length;
    }
    if (section != null) {
      throw section.error;
    }
    if (template.length !== pos) {
      buffer.push(function() {
        return template.slice(pos);
      });
    }
    return cache[template] = buffer;
  };
  Milk = {
    VERSION: '1.2.0',
    helpers: [],
    partials: null,
    escape: function(value) {
      var entities;
      entities = {
        '&': 'amp',
        '"': 'quot',
        '<': 'lt',
        '>': 'gt'
      };
      return value.replace(/[&"<>]/g, function(ch) {
        return "&" + entities[ch] + ";";
      });
    },
    render: function(template, data, partials) {
      var context;
      if (partials == null) {
        partials = null;
      }
      if (!((partials || (partials = this.partials || {})) instanceof Function)) {
        partials = (function(partials) {
          return function(name) {
            if (!(name in partials)) {
              throw "Unknown partial '" + name + "'!";
            }
            return Find(name, [partials]);
          };
        })(partials);
      }
      context = this.helpers instanceof Array ? this.helpers : [this.helpers];
      return Expand(this, Parse(template), context.concat([data]), partials);
    }
  };
  if (typeof exports != "undefined" && exports !== null) {
    for (key in Milk) {
      exports[key] = Milk[key];
    }
  } else {
    this.Milk = Milk;
  }
}).call(this);
</script>

<script src="http://terrainformatica.com/kite/kite.js"></script>

<script src="http://github.com/downloads/wycats/handlebars.js/handlebars-0.9.0.pre.4.js"></script>

<!-- https://gist.githubusercontent.com/jashkenas/550881/raw/29bb186167079c0b33ab6e9d50d779f37860cfa4/micro.js -->
<script>
  (function() {
    var cache = {};

    this.tmpl = function tmpl(str, data) {
        // Figure out if we're getting a template, or if we need to
        // load the template - and be sure to cache the result.
        var fn = !/\W/.test(str) ?
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :

        // Generate a reusable function that will serve as a template
        // generator (and which will be cached).
      new Function("obj",
        "var p=[],print=function(){p.push.apply(p,arguments);};" +

        // Introduce the data as local variables using with(){}
        "with(obj){p.push('" +

        // Convert the template into pure JavaScript
str.replace(/[\r\t\n]/g, " ")
   .replace(/'(?=[^%]*%>)/g,"\t")
   .split("'").join("\\'")
   .split("\t").join("'")
   .replace(/<%=(.+?)%>/g, "',$1,'")
   .split("<%").join("');")
   .split("%>").join("p.push('")
   + "');}return p.join('');");
        // Provide some basic currying to the user
        return data ? fn(data) : fn;
    };
})();
</script>

<!-- https://gist.githubusercontent.com/akorchev/860205/raw/8444586913ab249c619671b8f5054fc92dddf643/micro2.js -->
<script>
  (function() {
    var cache = {};

    this.tmpl2 = function tmpl(str, data) {
        // Figure out if we're getting a template, or if we need to
        // load the template - and be sure to cache the result.
        var fn = !/\W/.test(str) ?
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :

        // Generate a reusable function that will serve as a template
        // generator (and which will be cached).
      new Function("data",
        "var p=[];" +

        // Introduce the data as local variables using with(){}
        "p.push('" +

        // Convert the template into pure JavaScript
str.replace(/[\r\t\n]/g, " ")
   .replace(/'(?=[^%]*%>)/g,"\t")
   .split("'").join("\\'")
   .split("\t").join("'")
   .replace(/<%=(.+?)%>/g, "',$1,'")
   .split("<%").join("');")
   .split("%>").join("p.push('")
   + "');return p.join('');");
        // Provide some basic currying to the user
        return data ? fn(data) : fn;
    };
})();
</script>

<!-- https://gist.githubusercontent.com/akorchev/860240/raw/cd98cacbdeee7eb2cfb2ca3ca76638dae2a5b1af/micro3.js -->
<script>
  (function() {
    this.tmpl3 = function tmpl(str, data) {
        var value = "var out = ''; out+=" + "'" +

        str.replace(/[\r\t\n]/g, " ")
           .replace(/'(?=[^%]*%>)/g,"\t")
           .split("'").join("\\'")
           .split("\t").join("'")
           .replace(/<%=(.+?)%>/g, "'; out += $1; out += '")
           .split("<%").join("';")
           .split("%>").join("out+='")
           + "'; return out;";
           
           return new Function("data", value);
   }
})();
</script>

<!-- http://github.com/creationix/haml-js/raw/master/lib/haml.js -->
<script>
// <![CDATA[
  var Haml;
 
(function () {

  var matchers, self_close_tags, embedder, forceXML, escaperName, escapeHtmlByDefault;

  function html_escape(text) {
    return (text + "").
      replace(/&/g, "&amp;").
      replace(/</g, "&lt;").
      replace(/>/g, "&gt;").
      replace(/\"/g, "&quot;");
  }

  function render_attribs(attribs) {
    var key, value, result = [];
    for (key in attribs) {
      if (key !== '_content' && attribs.hasOwnProperty(key)) {
        switch (attribs[key]) {
        case 'undefined':
        case 'false':
        case 'null':
        case '""':
          break;
        default:
          try {
            value = JSON.parse("[" + attribs[key] +"]")[0];
            if (value === true) {
              value = key;
            } else if (typeof value === 'string' && embedder.test(value)) {
              value = '" +\n' + parse_interpol(html_escape(value)) + ' +\n"';
            } else {
              value = html_escape(value);
            }
            result.push(" " + key + '=\\"' + value + '\\"');
          } catch (e) {
            result.push(" " + key + '=\\"" + '+escaperName+'(' + attribs[key] + ') + "\\"');
          }
        }
      }
    }
    return result.join("");
  }

  // Parse the attribute block using a state machine
  function parse_attribs(line) {
    var attributes = {},
        l = line.length,
        i, c,
        count = 1,
        quote = false,
        skip = false,
        open, close, joiner, seperator,
        pair = {
          start: 1,
          middle: null,
          end: null
        };

    if (!(l > 0 && (line.charAt(0) === '{' || line.charAt(0) === '('))) {
      return {
        _content: line[0] === ' ' ? line.substr(1, l) : line
      };
    }
    open = line.charAt(0);
    close = (open === '{') ? '}' : ')';
    joiner = (open === '{') ? ':' : '=';
    seperator = (open === '{') ? ',' : ' ';

    function process_pair() {
      if (typeof pair.start === 'number' &&
          typeof pair.middle === 'number' &&
          typeof pair.end === 'number') {
        var key = line.substr(pair.start, pair.middle - pair.start).trim(),
            value = line.substr(pair.middle + 1, pair.end - pair.middle - 1).trim();
        attributes[key] = value;
      }
      pair = {
        start: null,
        middle: null,
        end: null
      };
    }

    for (i = 1; count > 0; i += 1) {

      // If we reach the end of the line, then there is a problem
      if (i > l) {
        throw "Malformed attribute block";
      }

      c = line.charAt(i);
      if (skip) {
        skip = false;
      } else {
        if (quote) {
          if (c === '\\') {
            skip = true;
          }
          if (c === quote) {
            quote = false;
          }
        } else {
          if (c === '"' || c === "'") {
            quote = c;
          }

          if (count === 1) {
            if (c === joiner) {
              pair.middle = i;
            }
            if (c === seperator || c === close) {
              pair.end = i;
              process_pair();
              if (c === seperator) {
                pair.start = i + 1;
              }
            }
          }

          if (c === open || c === "(") {
            count += 1;
          }
          if (c === close || (count > 1 && c === ")")) {
            count -= 1;
          }
        }
      }
    }
    attributes._content = line.substr(i, line.length);
    return attributes;
  }

  // Split interpolated strings into an array of literals and code fragments.
  function parse_interpol(value) {
    var items = [],
        pos = 0,
        next = 0,
        match;
    while (true) {
      // Match up to embedded string
      next = value.substr(pos).search(embedder);
      if (next < 0) {
        if (pos < value.length) {
          items.push(JSON.stringify(value.substr(pos)));
        }
        break;
      }
      items.push(JSON.stringify(value.substr(pos, next)));
      pos += next;

      // Match embedded string
      match = value.substr(pos).match(embedder);
      next = match[0].length;
      if (next < 0) { break; }
      if(match[1] === "#"){
        items.push(escaperName+"("+(match[2] || match[3])+")");
      }else{
        //unsafe!!!
        items.push(match[2] || match[3]);
      }
      
      pos += next;
    }
    return items.filter(function (part) { return part && part.length > 0}).join(" +\n");
  }

  // Used to find embedded code in interpolated strings.
  embedder = /([#!])\{([^}]*)\}/;

  self_close_tags = ["meta", "img", "link", "br", "hr", "input", "area", "base"];

  // All matchers' regexps should capture leading whitespace in first capture
  // and trailing content in last capture
  matchers = [
    // html tags
    {
      name: "html tags",
      regexp: /^(\s*)((?:[.#%][a-z_\-][a-z0-9_:\-]*)+)(.*)$/i,
      process: function () {
        var line_beginning, tag, classes, ids, attribs, content, whitespaceSpecifier, whitespace={}, output;
        line_beginning = this.matches[2];
        classes = line_beginning.match(/\.([a-z_\-][a-z0-9_\-]*)/gi);
        ids = line_beginning.match(/\#([a-z_\-][a-z0-9_\-]*)/gi);
        tag = line_beginning.match(/\%([a-z_\-][a-z0-9_:\-]*)/gi);

        // Default to <div> tag
        tag = tag ? tag[0].substr(1, tag[0].length) : 'div';

        attribs = this.matches[3];
        if (attribs) {
          attribs = parse_attribs(attribs);
          if (attribs._content) {
            var leader0 = attribs._content.charAt(0),
                leader1 = attribs._content.charAt(1),
                leaderLength = 0;
                
            if(leader0 == "<"){
              leaderLength++;
              whitespace.inside = true;
              if(leader1 == ">"){
                leaderLength++;
                whitespace.around = true;
              }
            }else if(leader0 == ">"){
              leaderLength++;
              whitespace.around = true;
              if(leader1 == "<"){
                leaderLength++;
                whitespace.inside = true;
              }
            }
            attribs._content = attribs._content.substr(leaderLength);
            //once we've identified the tag and its attributes, the rest is content.
            // this is currently trimmed for neatness.
            this.contents.unshift(attribs._content.trim());
            delete(attribs._content);
          }
        } else {
          attribs = {};
        }

        if (classes) {
          classes = classes.map(function (klass) {
            return klass.substr(1, klass.length);
          }).join(' ');
          if (attribs['class']) {
            try {
              attribs['class'] = JSON.stringify(classes + " " + JSON.parse(attribs['class']));
            } catch (e) {
              attribs['class'] = JSON.stringify(classes + " ") + " + " + attribs['class'];
            }
          } else {
            attribs['class'] = JSON.stringify(classes);
          }
        }
        if (ids) {
          ids = ids.map(function (id) {
            return id.substr(1, id.length);
          }).join(' ');
          if (attribs.id) {
            attribs.id = JSON.stringify(ids + " ") + attribs.id;
          } else {
            attribs.id = JSON.stringify(ids);
          }
        }

        attribs = render_attribs(attribs);

        content = this.render_contents();
        if (content === '""') {
          content = '';
        }
        
        if(whitespace.inside){
          if(content.length==0){
            content='"  "'
          }else{
            try{ //remove quotes if they are there
              content = '" '+JSON.parse(content)+' "';
            }catch(e){
              content = '" "+\n'+content+'+\n" "';
            }            
          }
        }

        if (forceXML ? content.length > 0 : self_close_tags.indexOf(tag) == -1) {
          output = '"<' + tag + attribs + '>"' +
            (content.length > 0 ? ' + \n' + content : "") +
            ' + \n"</' + tag + '>"';
        } else {
          output = '"<' + tag + attribs + ' />"';
        }
        
        if(whitespace.around){
          //output now contains '"<b>hello</b>"'
          //we need to crack it open to insert whitespace.
          output = '" '+output.substr(1, output.length - 2)+' "';
        }

        return output;
      }
    },

    // each loops
    {
      name: "each loop",
      regexp: /^(\s*)(?::for|:each)\s+(?:([a-z_][a-z_\-]*),\s*)?([a-z_][a-z_\-]*)\s+in\s+(.*)(\s*)$/i,
      process: function () {
        var ivar = this.matches[2] || '__key__', // index
            vvar = this.matches[3],              // value
            avar = this.matches[4],              // array
            rvar = '__result__';                 // results

        if (this.matches[5]) {
          this.contents.unshift(this.matches[5]);
        }
        return '(function () { ' +
          'var ' + rvar + ' = [], ' + ivar + ', ' + vvar + '; ' +
          'for (' + ivar + ' in ' + avar + ') { ' +
          'if (' + avar + '.hasOwnProperty(' + ivar + ')) { ' +
          vvar + ' = ' + avar + '[' + ivar + ']; ' +
          rvar + '.push(\n' + (this.render_contents() || "''") + '\n); ' +
          '} } return ' + rvar + '.join(""); }).call(this)';
      }
    },

    // if statements
    {
      name: "if",
      regexp: /^(\s*):if\s+(.*)\s*$/i,
      process: function () {
        var condition = this.matches[2];
        this.pushIfCondition([condition]);
        return '(function () { ' +
          'if (' + condition + ') { ' +
          'return (\n' + (this.render_contents() || '') + '\n);' +
          '} else { return ""; } }).call(this)';
      }
    },
    
    // else if statements
    {
      name: "else if",
      regexp: /^(\s*):else if\s+(.*)\s*$/i,
      process: function () {
        var condition = this.matches[2],
          conditionsArray = this.getIfConditions()[this.getIfConditions().length - 1],
          ifArray = [],
          ifStatement;
        for (var i=0, l=conditionsArray.length; i<l; i++) {
          ifArray.push('! (' + conditionsArray[i]+')');
        }
        conditionsArray.push(condition);
        ifArray.push(condition);
        ifStatement = 'if (' + ifArray.join(' && ') + ') { ';
        return '(function () { ' +
          ifStatement +
          'return (\n' + (this.render_contents() || '') + '\n);' +
          '} else { return ""; } }).call(this)';
      }
    },
    
    // else statements
    {
      name: "else",
      regexp: /^(\s*):else\s*$/i,
      process: function () {
        var conditionsArray = this.popIfCondition(),
          ifArray = [],
          ifStatement;
        for (var i=0, l=conditionsArray.length; i<l; i++) {
          ifArray.push('! (' + conditionsArray[i]+')');
        }
        ifStatement = 'if (' + ifArray.join(' && ') + ') { ';
        return '(function () { ' +
          ifStatement +
          'return (\n' + (this.render_contents() || '') + '\n);' +
          '} else { return ""; } }).call(this)';
      }
    },
    
    // silent-comments
    {
      name: "silent-comments",
      regexp: /^(\s*)-#\s*(.*)\s*$/i,
      process: function () {
        return '""';
      }
    },
    
    //html-comments
    {
      name: "silent-comments",
      regexp: /^(\s*)\/\s*(.*)\s*$/i,
      process: function () {
        this.contents.unshift(this.matches[2]);
        
        return '"<!--'+this.contents.join('\\n').replace(/\"/g, '\\"')+'-->"';
      }
    },
    
    // raw js
    {
      name: "rawjs",
      regexp: /^(\s*)-\s*(.*)\s*$/i,
      process: function () {
        this.contents.unshift(this.matches[2]);
        return '"";' + this.contents.join("\n")+"; _$output = _$output ";
      }
    },

    // raw js
    {
      name: "pre",
      regexp: /^(\s*):pre(\s+(.*)|$)/i,
      process: function () {
        this.contents.unshift(this.matches[2]);
        return '"<pre>"+\n' + JSON.stringify(this.contents.join("\n"))+'+\n"</pre>"';
      }
    },
    
    // declarations
    {
      name: "doctype",
      regexp: /^()!!!(?:\s*(.*))\s*$/,
      process: function () {
        var line = '';
        switch ((this.matches[2] || '').toLowerCase()) {
        case '':
          // XHTML 1.0 Transitional
          line = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
          break;
        case 'strict':
        case '1.0':
          // XHTML 1.0 Strict
          line = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
          break;
        case 'frameset':
          // XHTML 1.0 Frameset
          line = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">';
          break;
        case '5':
          // XHTML 5
          line = '<!DOCTYPE html>';
          break;
        case '1.1':
          // XHTML 1.1
          line = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">';
          break;
        case 'basic':
          // XHTML Basic 1.1
          line = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">';
          break;
        case 'mobile':
          // XHTML Mobile 1.2
          line = '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">';
          break;
        case 'xml':
          // XML
          line = "<?xml version='1.0' encoding='utf-8' ?>";
          break;
        case 'xml iso-8859-1':
          // XML iso-8859-1
          line = "<?xml version='1.0' encoding='iso-8859-1' ?>";
          break;
        }
        return JSON.stringify(line + "\n");
      }
    },

    // Embedded markdown. Needs to be added to exports externally.
    {
      name: "markdown",
      regexp: /^(\s*):markdown\s*$/i,
      process: function () {
        return parse_interpol(exports.Markdown.encode(this.contents.join("\n")));
      }
    },

    // script blocks
    {
      name: "script",
      regexp: /^(\s*):(?:java)?script\s*$/,
      process: function () {
        return parse_interpol('\n<script>\n' +
          '//<![CDATA[\n' +
          this.contents.join("\n") +
          "\n//]]>\n<\/script>\n");
      }
    },

    // css blocks
    {
      name: "css",
      regexp: /^(\s*):css\s*$/,
      process: function () {
        return JSON.stringify('<style type="text/css">\n' +
          this.contents.join("\n") +
          "\n<\/style>");
      }
    }

  ];

  function compile(lines) {
    var block = false,
        output = [],
        ifConditions = [];

    // If lines is a string, turn it into an array
    if (typeof lines === 'string') {
      lines = lines.trim().replace(/\n\r|\r/g, '\n').split('\n');
    }

    lines.forEach(function(line) {
      var match, found = false;

      // Collect all text as raw until outdent
      if (block) {
        match = block.check_indent.exec(line);
        if (match) {
          block.contents.push(match[1] || "");
          return;
        } else {
          output.push(block.process());
          block = false;
        }
      }

      matchers.forEach(function (matcher) {
        if (!found) {
          match = matcher.regexp.exec(line);
          if (match) {
            block = {
              contents: [],
              indent_level: (match[1]),
              matches: match,
              check_indent: new RegExp("^(?:\\s*|" + match[1] + "  (.*))$"),
              process: matcher.process,
              getIfConditions: function() {
                return ifConditions;
              },
              pushIfCondition: function(condition) {
                ifConditions.push(condition);
              },
              popIfCondition: function() {
                return ifConditions.pop();
              },
              render_contents: function () {
                return compile(this.contents);
              }
            };
            found = true;
          }
        }
      });
      
      // Match plain text
      if (!found) {
        output.push(function () {
          // Escaped plain text
          if (line[0] === '\\') {
            return parse_interpol(line.substr(1, line.length));
          }


          function escapedLine(){
            try {
              return escaperName+'('+JSON.stringify(JSON.parse(line)) +')';
            } catch (e2) {
              return escaperName+'(' + line + ')';
            }
          }
          
          function unescapedLine(){
            try {
              return parse_interpol(JSON.parse(line));
            } catch (e) {
              return line;
            }
          }
          
          // always escaped
          if((line.substr(0, 2) === "&=")) {
            line = line.substr(2, line.length).trim();
            return escapedLine();
          }
          
          //never escaped
          if((line.substr(0, 2) === "!=")) {
            line = line.substr(2, line.length).trim();
            return unescapedLine();
          }
          
          // sometimes escaped
          if ( (line[0] === '=')) {
            line = line.substr(1, line.length).trim();
            if(escapeHtmlByDefault){
              return escapedLine();
            }else{
              return unescapedLine();
            }
          }

          // Plain text
          return parse_interpol(line);
        }());
      }

    });
    if (block) {
      output.push(block.process());
    }
    
    var txt = output.filter(function (part) { return part && part.length > 0}).join(" +\n");
    if(txt.length == 0){
      txt = '""';
    }
    return txt;
  };

  function optimize(js) {
    var new_js = [], buffer = [], part, end;

    function flush() {
      if (buffer.length > 0) {
        new_js.push(JSON.stringify(buffer.join("")) + end);
        buffer = [];
      }
    }
    js.replace(/\n\r|\r/g, '\n').split('\n').forEach(function (line) {
      part = line.match(/^(\".*\")(\s*\+\s*)?$/);
      if (!part) {
        flush();
        new_js.push(line);
        return;
      }
      end = part[2] || "";
      part = part[1];
      try {
        buffer.push(JSON.parse(part));
      } catch (e) {
        flush();
        new_js.push(line);
      }
    });
    flush();
    return new_js.join("\n");
  };

  function render(text, options) {
    options = options || {};
    text = text || "";
    var js = compile(text, options);
    if (options.optimize) {
      js = Haml.optimize(js);
    }
    return execute(js, options.context || Haml, options.locals);
  };

  function execute(js, self, locals) {
    return (function () {
      with(locals || {}) {
        try {
          var _$output;
          eval("_$output =" + js );
          return _$output; //set in eval
        } catch (e) {
          return "\n<pre class='error'>" + html_escape(e.stack) + "</pre>\n";
        }

      }
    }).call(self);
  };

  Haml = function Haml(haml, config) {
    if(typeof(config) != "object"){
      forceXML = config;
      config = {};
    }
    
    var escaper;
    if(config.customEscape){
      escaper = "";
      escaperName = config.customEscape;
    }else{
      escaper = html_escape.toString() + "\n";
      escaperName = "html_escape";
    }
    
    escapeHtmlByDefault = (config.escapeHtmlByDefault || config.escapeHTML || config.escape_html);
    
    var js = optimize(compile(haml));
    
    var str = "with(locals || {}) {\n" +
    "  try {\n" +
    "   var _$output=" + js + ";\n return _$output;" +
    "  } catch (e) {\n" +
    "    return \"\\n<pre class='error'>\" + "+escaperName+"(e.stack) + \"</pre>\\n\";\n" +
    "  }\n" +
    "}"

    try{
      var f = new Function("locals",  escaper + str );
      return f;
    }catch(e){
      if ( typeof(console) !== 'undefined' ) { console.error(str); }
      throw e;
    }
  }

  Haml.compile = compile;
  Haml.optimize = optimize;
  Haml.render = render;
  Haml.execute = execute;
  Haml.html_escape = html_escape;
}());

// Hook into module system
if (typeof module !== 'undefined') {
  module.exports = Haml;
}
//]]>
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/coffee-script/1.7.1/coffee-script.min.js"></script>

<!-- http://sstephenson.github.com/eco/dist/eco.js -->
<script>
  /**
 * Eco Compiler v1.1.0-pre
 * http://github.com/sstephenson/eco
 *
 * Copyright (c) 2010 Sam Stephenson
 * Released under the MIT License
 */
this.eco=function(j){return function g(h){var b,c={id:h,exports:{}};if(b=j[h]){b(c,g,c.exports);return c.exports}else throw"Cannot find module '"+h+"'";}}({eco:function(j,g){g("coffee-script");j.exports=g("eco/compiler")},"eco/compiler":function(j,g){(function(){var h,b,c,d,f;h=g("coffee-script");d=g("eco/util").indent;j.exports=c=function(a){(new Function("module",b(a)))(a={});return a.exports};c.preprocess=f=g("eco/preprocessor").preprocess;c.compile=b=function(a,e){var i,k;k=typeof(i=typeof e===
"undefined"||e===null?undefined:e.identifier)!=="undefined"&&i!==null?i:"module.exports";k.match(/\./)||(k="var "+k);i=h.compile(f(a),{noWrap:true});return""+k+" = function(__obj) {\n  if (!__obj) __obj = {};\n  var __out = [], __capture = function(callback) {\n    var out = __out, result;\n    __out = [];\n    callback.call(this);\n    result = __out.join('');\n    __out = out;\n    return __safe(result);\n  }, __sanitize = function(value) {\n    if (value && value.ecoSafe) {\n      return value;\n    } else if (typeof value !== 'undefined' && value != null) {\n      return __escape(value);\n    } else {\n      return '';\n    }\n  }, __safe, __objSafe = __obj.safe, __escape = __obj.escape;\n  __safe = __obj.safe = function(value) {\n    if (value && value.ecoSafe) {\n      return value;\n    } else {\n      if (!(typeof value !== 'undefined' && value != null)) value = '';\n      var result = new String(value);\n      result.ecoSafe = true;\n      return result;\n    }\n  };\n  if (!__escape) {\n    __escape = __obj.escape = function(value) {\n      return ('' + value)\n        .replace(/&/g, '&amp;')\n        .replace(/</g, '&lt;')\n        .replace(/>/g, '&gt;')\n        .replace(/\"/g, '&quot;');\n    };\n  }\n  (function() {\n"+
d(i,4)+"\n  }).call(__obj);\n  __obj.safe = __objSafe, __obj.escape = __escape;\n  return __out.join('');\n};"};c.render=function(a,e){return c(a)(e)};if(g.extensions)g.extensions[".eco"]=function(a,e){var i;i=g("fs").readFileSync(e,"utf-8");return a._compile(b(i),e)};else g.registerExtension&&g.registerExtension(".eco",b)}).call(this)},"eco/preprocessor":function(j,g,h){(function(){var b,c,d,f=function(a,e){return function(){return a.apply(e,arguments)}};c=g("eco/scanner").Scanner;d=g("eco/util");
h.preprocess=function(a){return(new b(a)).preprocess()};h.Preprocessor=function(){b=function(a){this.scanner=new c(a);this.output="";this.level=0;this.options={};this.captures=[];return this};b.prototype.preprocess=function(){for(;!this.scanner.done;)this.scanner.scan(f(function(a){return this[a[0]].apply(this,a.slice(1))},this));return this.output};b.prototype.record=function(a){this.output+=d.repeat("  ",this.level);return this.output+=a+"\n"};b.prototype.printString=function(a){return a.length?
this.record("__out.push "+d.inspectString(a)):null};b.prototype.beginCode=function(a){return this.options=a};b.prototype.recordCode=function(a){return a!=="end"?this.options.print?this.options.safe?this.record("__out.push "+a):this.record("__out.push __sanitize "+a):this.record(a):null};b.prototype.indent=function(a){this.level++;if(a){this.record("__capture "+a);this.captures.unshift(this.level);return this.indent()}};b.prototype.dedent=function(){this.level--;this.level<0&&this.fail("unexpected dedent");
if(this.captures[0]===this.level){this.captures.shift();return this.dedent()}};b.prototype.fail=function(a){throw"Parse error on line "+this.scanner.lineNo+": "+a;};return b}()}).call(this)},"eco/scanner":function(j,g,h){(function(){var b,c,d,f;d=g("strscan");c=d.StringScanner;d=g("eco/util");f=d.trim;h.scan=function(a){var e;e=[];for(a=new b(a);!a.done;)a.scan(function(i){return e.push(i)});return e};h.Scanner=function(){b=function(a){this.source=a.replace(/\r\n?/g,"\n");this.scanner=new c(this.source);
this.mode="data";this.buffer="";this.lineNo=1;this.done=false;return this};b.modePatterns={data:/(.*?)(<%(([=-])?)|\n|$)/,code:/(.*?)(((:|(->|=>))\s*)?%>|\n|$)/};b.dedentablePattern=/^(end|when|else|catch|finally)(?:\W|$)/;b.prototype.scan=function(a){if(this.done)return a();else if(this.scanner.hasTerminated()){this.done=true;switch(this.mode){case "data":return a(["printString",this.flush()]);case "code":return a(["fail","unexpected end of template"])}}else{this.advance();switch(this.mode){case "data":return this.scanData(a);
case "code":return this.scanCode(a)}}};b.prototype.advance=function(){this.scanner.scanUntil(b.modePatterns[this.mode]);this.buffer+=this.scanner.getCapture(0);this.tail=this.scanner.getCapture(1);this.directive=this.scanner.getCapture(3);return this.arrow=this.scanner.getCapture(4)};b.prototype.scanData=function(a){var e;if(this.tail==="\n"){this.buffer+=this.tail;this.lineNo++;return this.scan(a)}else if(this.tail){this.mode="code";a(["printString",this.flush()]);return a(["beginCode",{print:typeof(e=
this.directive)!=="undefined"&&e!==null,safe:this.directive==="-"}])}};b.prototype.scanCode=function(a){var e;if(this.tail==="\n")return a(["fail","unexpected newline in code block"]);else if(this.tail){this.mode="data";e=f(this.flush());if(this.arrow)e+=" "+this.arrow;this.isDedentable(e)&&a(["dedent"]);a(["recordCode",e]);if(this.directive)return a(["indent",this.arrow])}};b.prototype.flush=function(){var a;a=this.buffer;this.buffer="";return a};b.prototype.isDedentable=function(a){return a.match(b.dedentablePattern)};
return b}.call(this)}).call(this)},"eco/util":function(j,g,h){(function(){var b,c;h.repeat=b=function(d,f){return Array(f+1).join(d)};h.indent=function(d,f){var a,e,i,k,l,m;m=b(" ",f);k=[];i=d.split("\n");a=0;for(e=i.length;a<e;a++){l=i[a];k.push(m+l)}return k.join("\n")};h.trim=function(d){return d.replace(/^\s+/,"").replace(/\s+$/,"")};c={"\\":"\\\\","\u0008":"\\b","\u000c":"\\f","\n":"\\n","\r":"\\r","\t":"\\t"};h.inspectString=function(d){return"'"+d.replace(/[\x00-\x1f\\]/g,function(f){if(f in
c)return c[f];else{f=f.charCodeAt(0).toString(16);if(f.length===1)f="0"+f;return"\\u00"+f}}).replace(/'/g,"\\'")+"'"}}).call(this)},strscan:function(j,g,h){(function(){var b;(typeof h!=="undefined"&&h!==null?h:this).StringScanner=function(){b=function(c){this.source=c.toString();this.reset();return this};b.prototype.scan=function(c){var d;return(d=c.exec(this.getRemainder()))&&d.index===0?this.setState(d,{head:this.head+d[0].length,last:this.head}):this.setState([])};b.prototype.scanUntil=function(c){if(c=
c.exec(this.getRemainder())){this.setState(c,{head:this.head+c.index+c[0].length,last:this.head});return this.source.slice(this.last,this.head)}else return this.setState([])};b.prototype.scanChar=function(){return this.scan(/./)};b.prototype.skip=function(c){if(this.scan(c))return this.match.length};b.prototype.skipUntil=function(c){if(this.scanUntil(c))return this.head-this.last};b.prototype.check=function(c){var d;return(d=c.exec(this.getRemainder()))&&d.index===0?this.setState(d):this.setState([])};
b.prototype.checkUntil=function(c){if(c=c.exec(this.getRemainder())){this.setState(c);return this.source.slice(this.head,this.head+c.index+c[0].length)}else return this.setState([])};b.prototype.peek=function(c){return this.source.substr(this.head,typeof c!=="undefined"&&c!==null?c:1)};b.prototype.getSource=function(){return this.source};b.prototype.getRemainder=function(){return this.source.slice(this.head)};b.prototype.getPosition=function(){return this.head};b.prototype.hasTerminated=function(){return this.head===
this.source.length};b.prototype.getPreMatch=function(){if(this.match)return this.source.slice(0,this.head-this.match.length)};b.prototype.getMatch=function(){return this.match};b.prototype.getPostMatch=function(){if(this.match)return this.source.slice(this.head)};b.prototype.getCapture=function(c){return this.captures[c]};b.prototype.reset=function(){return this.setState([],{head:0,last:0})};b.prototype.terminate=function(){return this.setState([],{head:this.source.length,last:this.head})};b.prototype.concat=
function(c){return this.source+=c};b.prototype.unscan=function(){if(this.match)return this.setState([],{head:this.last,last:0});else throw"nothing to unscan";};b.prototype.setState=function(c,d){var f,a;this.head=typeof(f=typeof d==="undefined"||d===null?undefined:d.head)!=="undefined"&&f!==null?f:this.head;this.last=typeof(a=typeof d==="undefined"||d===null?undefined:d.last)!=="undefined"&&a!==null?a:this.last;this.captures=c.slice(1);return this.match=c[0]};return b}()})()},"coffee-script":function(j){if(typeof CoffeeScript!==
"undefined"&&CoffeeScript!=null)j.exports=CoffeeScript;else throw"Cannot require '"+j.id+"': CoffeeScript not found";}})("eco");
</script>

<!-- https://raw.github.com/BorisMoore/jquery-tmpl/master/jquery.tmpl.min.js -->
<script>
  /*
 * jQuery Templates Plugin 1.0.0pre
 * http://github.com/jquery/jquery-tmpl
 * Requires jQuery 1.4.2
 *
 * Copyright 2011, Software Freedom Conservancy, Inc.
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 */
(function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},i=0,c=0,l=[];function g(g,d,h,e){var c={data:e||(e===0||e===false)?e:d?d.data:{},_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};g&&a.extend(c,g,{nodes:[],parent:d});if(h){c.tmpl=h;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++i;(l.length?f:b)[i]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h<m;h++){c=h;k=(h>0?this.clone(true):this).get();a(i[h])[d](k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,m,k){if(d[0]&&a.isArray(d[0])){var g=a.makeArray(arguments),h=d[0],j=h.length,i=0,f;while(i<j&&!(f=a.data(h[i++],"tmplItem")));if(f&&c)g[2]=function(b){a.tmpl.afterManip(this,b,k)};r.apply(this,g)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var i,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(j(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);i=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(j(c,null,i)):i},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){__=__.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(__,$1,$2);__=[];",close:"call=$item.calls();__=call._.concat($item.wrap(call,__));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){__.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){__.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function j(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:j(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=k(c).concat(b);if(d)b=b.concat(k(d))});return b?b:k(c)}function k(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,__=[],$data=$item.data;with($data){__.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,k,g,b,c,d){var j=a.tmpl.tag[k],i,e,f;if(!j)throw"Unknown template tag: "+k;i=j._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=h(b);d=d?","+h(d)+")":c?")":"";e=c?b.indexOf(".")>-1?b+h(c):"("+b+").call($item"+d:b;f=c?e:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else f=e=i.$1||"null";g=h(g);return"');"+j[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(f).split("$1").join(e).split("$2").join(g||i.$2||"")+"__.push('"})+"');}return __;")}function n(c,b){c._wrap=j(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function h(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,h;for(e=0,p=o.length;e<p;e++){if((k=o[e]).nodeType!==1)continue;j=k.getElementsByTagName("*");for(h=j.length-1;h>=0;h--)m(j[h]);m(k)}function m(j){var p,h=j,k,e,m;if(m=j.getAttribute(d)){while(h.parentNode&&(h=h.parentNode).nodeType===1&&!(p=h.getAttribute(d)));if(p!==m){h=h.parentNode?h.nodeType===11?0:h.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[h]||f[h]);e.key=++i;b[i]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;h=a.data(j.parentNode,"tmplItem");h=h?h.key:0}if(e){k=e;while(k&&k.key!=h){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery);
</script>

<!-- https://github.com/olado/doT/raw/master/doT.js -->
<script>
// doT.js
// 2011, Laura Doktorova, https://github.com/olado/doT
// Licensed under the MIT license.

(function() {
  "use strict";

  var doT = {
    version: '1.0.1',
    templateSettings: {
      evaluate:    /\{\{([\s\S]+?(\}?)+)\}\}/g,
      interpolate: /\{\{=([\s\S]+?)\}\}/g,
      encode:      /\{\{!([\s\S]+?)\}\}/g,
      use:         /\{\{#([\s\S]+?)\}\}/g,
      useParams:   /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
      define:      /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
      defineParams:/^\s*([\w$]+):([\s\S]+)/,
      conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
      iterate:     /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
      varname:  'it',
      strip:    true,
      append:   true,
      selfcontained: false
    },
    template: undefined, //fn, compile template
    compile:  undefined  //fn, for express
  }, global;

  if (typeof module !== 'undefined' && module.exports) {
    module.exports = doT;
  } else if (typeof define === 'function' && define.amd) {
    define(function(){return doT;});
  } else {
    global = (function(){ return this || (0,eval)('this'); }());
    global.doT = doT;
  }

  function encodeHTMLSource() {
    var encodeHTMLRules = { "&": "&#38;", "<": "&#60;", ">": "&#62;", '"': '&#34;', "'": '&#39;', "/": '&#47;' },
      matchHTML = /&(?!#?\w+;)|<|>|"|'|\//g;
    return function() {
      return this ? this.replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : this;
    };
  }
  String.prototype.encodeHTML = encodeHTMLSource();

  var startend = {
    append: { start: "'+(",      end: ")+'",      endencode: "||'').toString().encodeHTML()+'" },
    split:  { start: "';out+=(", end: ");out+='", endencode: "||'').toString().encodeHTML();out+='"}
  }, skip = /$^/;

  function resolveDefs(c, block, def) {
    return ((typeof block === 'string') ? block : block.toString())
    .replace(c.define || skip, function(m, code, assign, value) {
      if (code.indexOf('def.') === 0) {
        code = code.substring(4);
      }
      if (!(code in def)) {
        if (assign === ':') {
          if (c.defineParams) value.replace(c.defineParams, function(m, param, v) {
            def[code] = {arg: param, text: v};
          });
          if (!(code in def)) def[code]= value;
        } else {
          new Function("def", "def['"+code+"']=" + value)(def);
        }
      }
      return '';
    })
    .replace(c.use || skip, function(m, code) {
      if (c.useParams) code = code.replace(c.useParams, function(m, s, d, param) {
        if (def[d] && def[d].arg && param) {
          var rw = (d+":"+param).replace(/'|\\/g, '_');
          def.__exp = def.__exp || {};
          def.__exp[rw] = def[d].text.replace(new RegExp("(^|[^\\w$])" + def[d].arg + "([^\\w$])", "g"), "$1" + param + "$2");
          return s + "def.__exp['"+rw+"']";
        }
      });
      var v = new Function("def", "return " + code)(def);
      return v ? resolveDefs(c, v, def) : v;
    });
  }

  function unescape(code) {
    return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, ' ');
  }

  doT.template = function(tmpl, c, def) {
    c = c || doT.templateSettings;
    var cse = c.append ? startend.append : startend.split, needhtmlencode, sid = 0, indv,
      str  = (c.use || c.define) ? resolveDefs(c, tmpl, def || {}) : tmpl;

    str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g,' ')
          .replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,''): str)
      .replace(/'|\\/g, '\\$&')
      .replace(c.interpolate || skip, function(m, code) {
        return cse.start + unescape(code) + cse.end;
      })
      .replace(c.encode || skip, function(m, code) {
        needhtmlencode = true;
        return cse.start + unescape(code) + cse.endencode;
      })
      .replace(c.conditional || skip, function(m, elsecase, code) {
        return elsecase ?
          (code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") :
          (code ? "';if(" + unescape(code) + "){out+='" : "';}out+='");
      })
      .replace(c.iterate || skip, function(m, iterate, vname, iname) {
        if (!iterate) return "';} } out+='";
        sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);
        return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"
          +vname+"=arr"+sid+"["+indv+"+=1];out+='";
      })
      .replace(c.evaluate || skip, function(m, code) {
        return "';" + unescape(code) + "out+='";
      })
      + "';return out;")
      .replace(/\n/g, '\\n').replace(/\t/g, '\\t').replace(/\r/g, '\\r')
      .replace(/(\s|;|\}|^|\{)out\+='';/g, '$1').replace(/\+''/g, '')
      .replace(/(\s|;|\}|^|\{)out\+=''\+/g,'$1out+=');

    if (needhtmlencode && c.selfcontained) {
      str = "String.prototype.encodeHTML=(" + encodeHTMLSource.toString() + "());" + str;
    }
    try {
      return new Function(c.varname, str);
    } catch (e) {
      if (typeof console !== 'undefined') console.log("Could not create a template function: " + str);
      throw e;
    }
  };

  doT.compile = function(tmpl, def) {
    return doT.template(tmpl, null, def);
  };
}());
</script>

<!-- https://github.com/olado/doT/raw/master/doU.js -->
<script>
// doU.js
// (c) 2011, Laura Doktorova
// https://github.com/olado/doT
//
// doU is an extraction and slight modification of an excellent
// templating function from jQote2.js (jQuery plugin) by aefxx
// (http://aefxx.com/jquery-plugins/jqote2/).
//
// Modifications:
// 1. nodejs support
// 2. allow for custom template markers
// 3. only allow direct invocation of the compiled function
//
// Licensed under the MIT license.

(function() {
  var doU = { version : '0.1.2' };

  if (typeof module !== 'undefined' && module.exports) {
    module.exports = doU;
  } else {
    this.doU = doU;
  }

  doU.templateSettings = {
    begin : '{{',
    end : '}}',
    varname : 'it'
  };

  doU.template = function(tmpl, conf) {
    conf = conf || doU.templateSettings;
    var str = '', tb = conf.begin, te = conf.end, m, l,
      arr = tmpl.replace(/\s*<!\[CDATA\[\s*|\s*\]\]>\s*|[\r\n\t]|(\/\*[\s\S]*?\*\/)/g, '')
        .split(tb).join(te +'\x1b')
        .split(te);

    for (m=0,l=arr.length; m < l; m++) {
      str += arr[m].charAt(0) !== '\x1b' ?
      "out+='" + arr[m].replace(/(\\|["'])/g, '\\$1') + "'" : (arr[m].charAt(1) === '=' ?
      ';out+=(' + arr[m].substr(2) + ');' : (arr[m].charAt(1) === '!' ?
      ';out+=(' + arr[m].substr(2) + ").toString().replace(/&(?!\\w+;)/g, '&#38;').split('<').join('&#60;').split('>').join('&#62;').split('" + '"' + "').join('&#34;').split(" + '"' + "'" + '"' + ").join('&#39;').split('/').join('&#x2F;');" : ';' + arr[m].substr(1)));
    }

    str = ('var out="";'+str+';return out;')
      .split("out+='';").join('')
      .split('var out="";out+=').join('var out=');

    try {
      return new Function(conf.varname, str);
    } catch (e) {
      if (typeof console !== 'undefined') console.log("Could not create a template function: " + str);
      throw e;
    }
  };
}());
</script>

<!-- http://github.com/aefxx/jQote2/raw/69b2053a13f5f180e696de9b3dba856a3c00678c/jquery.jqote2.js -->
<script>
  /*
 * jQote2 - client-side Javascript templating engine
 * Copyright (C) 2010, aefxx
 * http://aefxx.com/
 *
 * Dual licensed under the WTFPL v2 or MIT (X11) licenses
 * WTFPL v2 Copyright (C) 2004, Sam Hocevar
 *
 * Date: Thu, Oct 21st, 2010
 * Version: 0.9.7
 */
(function($) {
    var JQOTE2_TMPL_UNDEF_ERROR = 'UndefinedTemplateError',
        JQOTE2_TMPL_COMP_ERROR  = 'TemplateCompilationError',
        JQOTE2_TMPL_EXEC_ERROR  = 'TemplateExecutionError';

    var ARR  = '[object Array]',
        STR  = '[object String]',
        FUNC = '[object Function]';

    var n = 1, tag = '%',
        qreg = /^[^<]*(<[\w\W]+>)[^>]*$/,
        type_of = Object.prototype.toString;

    function raise(error, ext) {
        throw ($.extend(error, ext), error);
    }

    function dotted_ns(fn) {
        var ns = [];

        if ( type_of.call(fn) !== ARR ) return false;

        for ( var i=0,l=fn.length; i < l; i++ )
            ns[i] = fn[i].jqote_id;

        return ns.length ?
            ns.sort().join('.').replace(/(\b\d+\b)\.(?:\1(\.|$))+/g, '$1$2') : false;
    }

    function lambda(tmpl, t) {
        var f, fn = [], t = t || tag,
            type = type_of.call(tmpl);

        if ( type === FUNC )
            return tmpl.jqote_id ? [tmpl] : false;

        if ( type !== ARR )
            return [$.jqotec(tmpl, t)];

        if ( type === ARR )
            for ( var i=0,l=tmpl.length; i < l; i++ )
                if ( f = lambda(tmpl[i], t) ) fn.push(f[0]);

        return fn.length ? fn : false;
    }

    $.fn.extend({
        jqote: function(data, t) {
            var data = type_of.call(data) === ARR ? data : [data],
                dom = '';

            this.each(function(i) {
                var fn = $.jqotec(this, t);

                for ( var j=0; j < data.length; j++ )
                    dom += fn.call(data[j], i, j, data, fn);
            });

            return dom;
        }
    });

    $.each({app: 'append', pre: 'prepend', sub: 'html'}, function(name, method) {
        $.fn['jqote'+name] = function(elem, data, t) {
            var ns, regexp, str = $.jqote(elem, data, t),
                $$ = !qreg.test(str) ?
                    function(str) {return $(document.createTextNode(str));} : $;

            if ( !!(ns = dotted_ns(lambda(elem))) )
                regexp = new RegExp('(^|\\.)'+ns.split('.').join('\\.(.*)?')+'(\\.|$)');

            return this.each(function() {
                var dom = $$(str);

                $(this)[method](dom);

                ( dom[0].nodeType === 3 ?
                    $(this) : dom ).trigger('jqote.'+name, [dom, regexp]);
            });
        };
    });

    $.extend({
        jqote: function(elem, data, t) {
            var str = '', t = t || tag,
                fn = lambda(elem);

            if ( fn === false )
                raise(new Error('Empty or undefined template passed to $.jqote'), {type: JQOTE2_TMPL_UNDEF_ERROR});

            data = type_of.call(data) !== ARR ?
                [data] : data;

            for ( var i=0,l=fn.length; i < l; i++ )
                for ( var j=0; j < data.length; j++ )
                    str += fn[i].call(data[j], i, j, data, fn[i]);

            return str;
        },

        jqotec: function(template, t) {
            var cache, elem, tmpl, t = t || tag,
                type = type_of.call(template);

            if ( type === STR && qreg.test(template) ) {
                elem = tmpl = template;

                if ( cache = $.jqotecache[template] ) return cache;
            } else {
                elem = type === STR || template.nodeType ?
                    $(template) : template instanceof jQuery ?
                        template : null;

                if ( !elem[0] || !(tmpl = elem[0].innerHTML) && !(tmpl = elem.text()) )
                    raise(new Error('Empty or undefined template passed to $.jqotec'), {type: JQOTE2_TMPL_UNDEF_ERROR});

                if ( cache = $.jqotecache[$.data(elem[0], 'jqote_id')] ) return cache;
            }

            var str = '', index,
                arr = tmpl.replace(/\s*<!\[CDATA\[\s*|\s*\]\]>\s*|[\r\n\t]/g, '')
                    .split('<'+t).join(t+'>\x1b')
                        .split(t+'>');

            for ( var m=0,l=arr.length; m < l; m++ )
                str += arr[m].charAt(0) !== '\x1b' ?
                    "out+='" + arr[m].replace(/(\\|["'])/g, '\\$1') + "'" : (arr[m].charAt(1) === '=' ?
                        ';out+=(' + arr[m].substr(2) + ');' : (arr[m].charAt(1) === '!' ?
                            ';out+=$.jqotenc((' + arr[m].substr(2) + '));' : ';' + arr[m].substr(1)));

            str = 'try{' +
                ('var out="";'+str+';return out;')
                    .split("out+='';").join('')
                        .split('var out="";out+=').join('var out=') +
                '}catch(e){e.type="'+JQOTE2_TMPL_EXEC_ERROR+'";e.args=arguments;e.template=arguments.callee.toString();throw e;}';

            try {
                var fn = new Function('i, j, data, fn', str);
            } catch ( e ) { raise(e, {type: JQOTE2_TMPL_COMP_ERROR}); }

            index = elem instanceof jQuery ?
                $.data(elem[0], 'jqote_id', n) : elem;

            return $.jqotecache[index] = (fn.jqote_id = n++, fn);
        },

        jqotefn: function(elem) {
            var type = type_of.call(elem),
                index = type === STR && qreg.test(elem) ?
                    elem : $.data($(elem)[0], 'jqote_id');

            return $.jqotecache[index] || false;
        },

        jqotetag: function(str) {
            if ( type_of.call(str) === STR ) tag = str;
        },

        jqotenc: function(str) {
            return s.toString()
                    .replace(/&(?!\w+;)/g, '&#38;')
                        .split('<').join('&#60;').split('>').join('&#62;')
                            .split('"').join('&#34;').split("'").join('&#39;');
        },

        jqotecache: {}
    });

    $.event.special.jqote = {
        add: function(obj) {
            var ns, handler = obj.handler,
                data = !obj.data ?
                    [] : type_of.call(obj.data) !== ARR ?
                        [obj.data] : obj.data;

            if ( !obj.namespace ) obj.namespace = 'app.pre.sub';
            if ( !data.length || !(ns = dotted_ns(lambda(data))) ) return;

            obj.handler = function(event, dom, regexp) {
                return !regexp || regexp.test(ns) ?
                    handler.apply(this, [event, dom]) : null;
            };
        }
    };
})(jQuery);

</script>
<!-- http://embeddedjavascript.googlecode.com/files/ejs_0.9_alpha_1_production.js -->
<script>
  String.prototype.rsplit=function(F){var E=this;var A=F.exec(E);var G=new Array();while(A!=null){var D=A.index;var C=F.lastIndex;if((D)!=0){var B=E.substring(0,D);G.push(E.substring(0,D));E=E.slice(D)}G.push(A[0]);E=E.slice(A[0].length);A=F.exec(E)}if(!E==""){G.push(E)}return G};String.prototype.chop=function(){return this.substr(0,this.length-1)};var EjsScanner=function(B,C,A){this.left_delimiter=C+"%";this.right_delimiter="%"+A;this.double_left=C+"%%";this.double_right="%%"+A;this.left_equal=C+"%=";this.left_comment=C+"%#";if(C=="["){this.SplitRegexp=/(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/}else{this.SplitRegexp=new RegExp("("+this.double_left+")|(%%"+this.double_right+")|("+this.left_equal+")|("+this.left_comment+")|("+this.left_delimiter+")|("+this.right_delimiter+"\n)|("+this.right_delimiter+")|(\n)")}this.source=B;this.stag=null;this.lines=0};EjsView=function(A){this.data=A};EjsView.prototype.partial=function(A,B){if(!B){B=this.data}return new EJS(A).render(B)};EjsScanner.to_text=function(A){if(A==null||A===undefined){return""}if(A instanceof Date){return A.toDateString()}if(A.toString){return A.toString()}return""};EjsScanner.prototype={scan:function(D){scanline=this.scanline;regex=this.SplitRegexp;if(!this.source==""){var C=this.source.rsplit(/\n/);for(var A=0;A<C.length;A++){var B=C[A];this.scanline(B,regex,D)}}},scanline:function(A,D,G){this.lines++;var E=A.rsplit(D);for(var C=0;C<E.length;C++){var B=E[C];if(B!=null){try{G(B,this)}catch(F){throw {type:"EjsScanner",line:this.lines}}}}}};var EjsBuffer=function(B,C){this.line=new Array();this.script="";this.pre_cmd=B;this.post_cmd=C;for(var A=0;A<this.pre_cmd.length;A++){this.push(B[A])}};EjsBuffer.prototype={push:function(A){this.line.push(A)},cr:function(){this.script=this.script+this.line.join("; ");this.line=new Array();this.script=this.script+"\n"},close:function(){if(this.line.length>0){for(var A=0;A<this.post_cmd.length;A++){this.push(pre_cmd[A])}this.script=this.script+this.line.join("; ");line=null}}};EjsCompiler=function(B,C){this.pre_cmd=['___ejsO = "";'];this.post_cmd=new Array();this.source=" ";if(B!=null){if(typeof B=="string"){B=B.replace(/\r\n/g,"\n");B=B.replace(/\r/g,"\n");this.source=B}else{if(B.innerHTML){this.source=B.innerHTML}}if(typeof this.source!="string"){this.source=""}}C=C||"<";var A=">";switch(C){case"[":A="]";break;case"<":break;default:throw C+" is not a supported deliminator";break}this.scanner=new EjsScanner(this.source,C,A);this.out=""};EjsCompiler.prototype={compile:function(options){options=options||{};this.out="";var put_cmd="___ejsO += ";var insert_cmd=put_cmd;var buff=new EjsBuffer(this.pre_cmd,this.post_cmd);var content="";var clean=function(content){content=content.replace(/\\/g,"\\\\");content=content.replace(/\n/g,"\\n");content=content.replace(/"/g,'\\"');return content};this.scanner.scan(function(token,scanner){if(scanner.stag==null){switch(token){case"\n":content=content+"\n";buff.push(put_cmd+'"'+clean(content)+'";');buff.cr();content="";break;case scanner.left_delimiter:case scanner.left_equal:case scanner.left_comment:scanner.stag=token;if(content.length>0){buff.push(put_cmd+'"'+clean(content)+'"')}content="";break;case scanner.double_left:content=content+scanner.left_delimiter;break;default:content=content+token;break}}else{switch(token){case scanner.right_delimiter:switch(scanner.stag){case scanner.left_delimiter:if(content[content.length-1]=="\n"){content=content.chop();buff.push(content);buff.cr()}else{buff.push(content)}break;case scanner.left_equal:buff.push(insert_cmd+"(EjsScanner.to_text("+content+"))");break}scanner.stag=null;content="";break;case scanner.double_right:content=content+scanner.right_delimiter;break;default:content=content+token;break}}});if(content.length>0){buff.push(put_cmd+'"'+clean(content)+'"')}buff.close();this.out=buff.script+";";var to_be_evaled="this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {"+this.out+" return ___ejsO;}}}catch(e){e.lineNumber=null;throw e;}};";try{eval(to_be_evaled)}catch(e){if(typeof JSLINT!="undefined"){JSLINT(this.out);for(var i=0;i<JSLINT.errors.length;i++){var error=JSLINT.errors[i];if(error.reason!="Unnecessary semicolon."){error.line++;var e=new Error();e.lineNumber=error.line;e.message=error.reason;if(options.url){e.fileName=options.url}throw e}}}else{throw e}}}};EJS=function(B){this.set_options(B);if(B.url){var C=EJS.get(B.url,this.cache);if(C){return C}if(C==EJS.INVALID_PATH){return null}this.text=EJS.request(B.url);if(this.text==null){throw"There is no template at "+B.url}this.name=B.url}else{if(B.element){if(typeof B.element=="string"){var A=B.element;B.element=document.getElementById(B.element);if(B.element==null){throw A+"does not exist!"}}if(B.element.value){this.text=B.element.value}else{this.text=B.element.innerHTML}this.name=B.element.id;this.type="["}}var C=new EjsCompiler(this.text,this.type);C.compile(B);EJS.update(this.name,this);this.template=C};EJS.config=function(B){EJS.cache=B.cache!=null?B.cache:EJS.cache;EJS.type=B.type!=null?B.type:EJS.type;var A={};EJS.get=function(D,C){if(C==false){return null}if(A[D]){return A[D]}return null};EJS.update=function(D,C){if(D==null){return }A[D]=C};EJS.INVALID_PATH=-1};EJS.config({cache:true,type:"<"});EJS.prototype={render:function(B){var A=new EjsView(B);return this.template.process.call(A,B,A)},out:function(){return this.template.out},set_options:function(A){this.type=A.type!=null?A.type:EJS.type;this.cache=A.cache!=null?A.cache:EJS.cache;this.text=A.text!=null?A.text:null;this.name=A.name!=null?A.name:null},update:function(element,options){if(typeof element=="string"){element=document.getElementById(element)}if(options==null){_template=this;return function(object){EJS.prototype.update.call(_template,element,object)}}if(typeof options=="string"){params={};params.url=options;_template=this;params.onComplete=function(request){var object=eval(request.responseText);EJS.prototype.update.call(_template,element,object)};EJS.ajax_request(params)}else{element.innerHTML=this.render(options)}}};EJS.newRequest=function(){var C=[function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new XMLHttpRequest()},function(){return new ActiveXObject("Microsoft.XMLHTTP")}];for(var A=0;A<C.length;A++){try{var B=C[A]();if(B!=null){return B}}catch(D){continue}}};EJS.request=function(C){var A=new EJS.newRequest();A.open("GET",C,false);try{A.send(null)}catch(B){return null}if(A.status==404||A.status==2||(A.status==0&&A.responseText=="")){return null}return A.responseText};EJS.ajax_request=function(B){B.method=(B.method?B.method:"GET");var A=new EJS.newRequest();A.onreadystatechange=function(){if(A.readyState==4){if(A.status==200){B.onComplete(A)}else{B.onComplete(A)}}};A.open(B.method,B.url);A.send(null)};EjsView.prototype.date_tag=function(C,O,A){if(!(O instanceof Date)){O=new Date()}var B=["January","February","March","April","May","June","July","August","September","October","November","December"];var G=[],D=[],P=[];var J=O.getFullYear();var H=O.getMonth();var N=O.getDate();for(var M=J-15;M<J+15;M++){G.push({value:M,text:M})}for(var E=0;E<12;E++){D.push({value:(E),text:B[E]})}for(var I=0;I<31;I++){P.push({value:(I+1),text:(I+1)})}var L=this.select_tag(C+"[year]",J,G,{id:C+"[year]"});var F=this.select_tag(C+"[month]",H,D,{id:C+"[month]"});var K=this.select_tag(C+"[day]",N,P,{id:C+"[day]"});return L+F+K};EjsView.prototype.form_tag=function(B,A){A=A||{};A.action=B;if(A.multipart==true){A.method="post";A.enctype="multipart/form-data"}return this.start_tag_for("form",A)};EjsView.prototype.form_tag_end=function(){return this.tag_end("form")};EjsView.prototype.hidden_field_tag=function(A,C,B){return this.input_field_tag(A,C,"hidden",B)};EjsView.prototype.input_field_tag=function(A,D,C,B){B=B||{};B.id=B.id||A;B.value=D||"";B.type=C||"text";B.name=A;return this.single_tag_for("input",B)};EjsView.prototype.is_current_page=function(A){return(window.location.href==A||window.location.pathname==A?true:false)};EjsView.prototype.link_to=function(B,A,C){if(!B){var B="null"}if(!C){var C={}}if(C.confirm){C.onclick=' var ret_confirm = confirm("'+C.confirm+'"); if(!ret_confirm){ return false;} ';C.confirm=null}C.href=A;return this.start_tag_for("a",C)+B+this.tag_end("a")};EjsView.prototype.submit_link_to=function(B,A,C){if(!B){var B="null"}if(!C){var C={}}C.onclick=C.onclick||"";if(C.confirm){C.onclick=' var ret_confirm = confirm("'+C.confirm+'"); if(!ret_confirm){ return false;} ';C.confirm=null}C.value=B;C.type="submit";C.onclick=C.onclick+(A?this.url_for(A):"")+"return false;";return this.start_tag_for("input",C)};EjsView.prototype.link_to_if=function(F,B,A,D,C,E){return this.link_to_unless((F==false),B,A,D,C,E)};EjsView.prototype.link_to_unless=function(E,B,A,C,D){C=C||{};if(E){if(D&&typeof D=="function"){return D(B,A,C,D)}else{return B}}else{return this.link_to(B,A,C)}};EjsView.prototype.link_to_unless_current=function(B,A,C,D){C=C||{};return this.link_to_unless(this.is_current_page(A),B,A,C,D)};EjsView.prototype.password_field_tag=function(A,C,B){return this.input_field_tag(A,C,"password",B)};EjsView.prototype.select_tag=function(D,G,H,F){F=F||{};F.id=F.id||D;F.value=G;F.name=D;var B="";B+=this.start_tag_for("select",F);for(var E=0;E<H.length;E++){var C=H[E];var A={value:C.value};if(C.value==G){A.selected="selected"}B+=this.start_tag_for("option",A)+C.text+this.tag_end("option")}B+=this.tag_end("select");return B};EjsView.prototype.single_tag_for=function(A,B){return this.tag(A,B,"/>")};EjsView.prototype.start_tag_for=function(A,B){return this.tag(A,B)};EjsView.prototype.submit_tag=function(A,B){B=B||{};B.type=B.type||"submit";B.value=A||"Submit";return this.single_tag_for("input",B)};EjsView.prototype.tag=function(C,E,D){if(!D){var D=">"}var B=" ";for(var A in E){if(E[A]!=null){var F=E[A].toString()}else{var F=""}if(A=="Class"){A="class"}if(F.indexOf("'")!=-1){B+=A+'="'+F+'" '}else{B+=A+"='"+F+"' "}}return"<"+C+B+D};EjsView.prototype.tag_end=function(A){return"</"+A+">"};EjsView.prototype.text_area_tag=function(A,C,B){B=B||{};B.id=B.id||A;B.name=B.name||A;C=C||"";if(B.size){B.cols=B.size.split("x")[0];B.rows=B.size.split("x")[1];delete B.size}B.cols=B.cols||50;B.rows=B.rows||4;return this.start_tag_for("textarea",B)+C+this.tag_end("textarea")};EjsView.prototype.text_tag=EjsView.prototype.text_area_tag;EjsView.prototype.text_field_tag=function(A,C,B){return this.input_field_tag(A,C,"text",B)};EjsView.prototype.url_for=function(A){return'window.location="'+A+'";'};EjsView.prototype.img_tag=function(B,C,A){A=A||{};A.src=B;A.alt=C;return this.single_tag_for("img",A)}
</script>
<!-- http://github.com/pure/pure/raw/master/libs/pure.js -->
<script>
  /*!
  PURE Unobtrusive Rendering Engine for HTML

  Licensed under the MIT licenses.
  More information at: http://www.opensource.org

  Copyright (c) 2013 Michael Cvilic - BeeBole.com

  Thanks to Rog Peppe for the functional JS jump
  revision: 2.83
*/

var $p = function(){
  var args = arguments,
    sel = args[0],
    ctxt = false;

  if(typeof sel === 'string'){
    ctxt = args[1] || false;
  }else if(sel && !sel[0] && !sel.length){
    sel = [sel];
  }
  return $p.core(sel, ctxt);
},
pure = $p;


$p.core = function(sel, ctxt, plugins){
  //get an instance of the plugins
  var templates = [], i, ii,
    // set the signature string that will be replaced at render time
    Sig = '_s' + Math.floor( Math.random() * 1000000 ) + '_',
    // another signature to prepend to attributes and avoid checks: style, height, on[events]...
    attPfx = '_a' + Math.floor( Math.random() * 1000000 ) + '_',
    // rx to parse selectors, e.g. "+tr.foo[class]"
    selRx = /^(\+)?([^\@\+]+)?\@?([^\+]+)?(\+)?$/,
    // set automatically attributes for some tags
    autoAttr = {
      IMG:'src',
      INPUT:'value'
    },
    // check if the argument is an array - thanks salty-horse (Ori Avtalion)
    isArray = Array.isArray ?
      function(o) {
        return Array.isArray(o);
      } :
      function(o) {
        return Object.prototype.toString.call(o) === "[object Array]";
      };

  plugins = plugins || getPlugins();

  //search for the template node(s)
  switch(typeof sel){
    case 'string':
      templates = plugins.find(ctxt || document, sel);
      if(templates.length === 0) {
        error('The template "' + sel + '" was not found');
      }
    break;
    case 'undefined':
      error('The root of the template is undefined, check your selector');
    break;
    default:
      templates = sel;
  }

  for( i = 0, ii = templates.length; i < ii; i++){
    plugins[i] = templates[i];
  }
  plugins.length = ii;

  /* * * * * * * * * * * * * * * * * * * * * * * * * *
    core functions
   * * * * * * * * * * * * * * * * * * * * * * * * * */


  // error utility
  function error(e){
    if(typeof console !== 'undefined'){
      console.log(e);
      //debugger;
    }
    throw('pure error: ' + e);
  }

  //return a new instance of plugins
  function getPlugins(){
    var plugins = $p.plugins,
      f = function(){};
    f.prototype = plugins;

    // do not overwrite functions if external definition
    f.prototype.compile    = plugins.compile || compile;
    f.prototype.render     = plugins.render || render;
    f.prototype.autoRender = plugins.autoRender || autoRender;
    f.prototype.find       = plugins.find || find;

    // give the compiler and the error handling to the plugin context
    f.prototype._compiler  = compiler;
    f.prototype._error     = error;

    return new f();
  }

  // returns the outer HTML of a node
  function outerHTML(node){
    // if IE, Chrome take the internal method otherwise build one
    return node.outerHTML || (
      function(n){
      var div = document.createElement('div'), h;
      div.appendChild( n.cloneNode(true) );
        h = div.innerHTML;
        div = null;
        return h;
      }(node));
  }

  // returns the string generator function
  function wrapquote(qfn, f){
    return function(ctxt){
      return qfn( String( f.call(ctxt.item || ctxt.context, ctxt) ) ) ;
    };
  }

  // default find using querySelector when available on the browser
  function find(n, sel){
    if(typeof n === 'string'){
      sel = n;
      n = false;
    }
    return (n||document).querySelectorAll( sel );
  }

  // create a function that concatenates constant string
  // sections (given in parts) and the results of called
  // functions to fill in the gaps between parts (fns).
  // fns[n] fills in the gap between parts[n-1] and parts[n];
  // fns[0] is unused.
  // this is the inner template evaluation loop.
  function concatenator(parts, fns){
    return function(ctxt){
      var strs = [ parts[ 0 ] ],
        n = parts.length,
        fnVal, pVal, attLine, pos, i;
      try{
        for(i = 1; i < n; i++){
          fnVal = fns[i].call( this, ctxt );
          pVal = parts[i];

          // if the value is empty and attribute, remove it
          if(fnVal === ''){
            attLine = strs[ strs.length - 1 ];
            if( ( pos = attLine.search( /[^\s]+=\"?$/ ) ) > -1){
              strs[ strs.length - 1 ] = attLine.substring( 0, pos );
              pVal = pVal.substr( 1 );
            }
          }

          strs[ strs.length ] = fnVal;
          strs[ strs.length ] = pVal;
        }
        return strs.join('');
      }catch(e){
        if(console && console.log){
          console.log( 
            e.stack || 
            e.message + ' (' + e.type + ( e['arguments'] ? ', ' + e['arguments'].join('-') : '' ) + '). Use Firefox or Chromium/Chrome to get a full stack of the error. ' );
        }
        return '';
      }
    };
  }

  // parse and check the loop directive
  function parseloopspec(p){
    var m = p.match( /^(\w+)\s*<-\s*(\S+)?$/ );
    if(m === null){
      error('bad loop spec: "' + p + '"');
    }
    if(m[1] === 'item'){
      error('"item<-..." is a reserved word for the current running iteration.\n\nPlease choose another name for your loop.');
    }
    if( !m[2] || m[2].toLowerCase() === 'context' ){ //undefined or space(IE)
      m[2] = function(ctxt){return ctxt.context;};
    }else if( (m[2] && m[2].indexOf('context') === 0 ) ){ //undefined or space(IE)
      m[2] = dataselectfn( m[2].replace(/^context\.?/, '') );
    }
    return {name: m[1], sel: m[2]};
  }

  // parse a data selector and return a function that
  // can traverse the data accordingly, given a context.
  function dataselectfn (sel){
    if( typeof(sel) === 'function' ){
      //handle false values in function directive
      return function ( ctxt ){
        var r = sel.call( ctxt.item || ctxt.context || ctxt, ctxt ); 
        return !r && r !== 0 ? '' : r;
      };
    }
    //check for a valid js variable name with hyphen(for properties only), $, _ and :
    var m = sel.match(/^[\da-zA-Z\$_\@\#][\w\$:\-\#]*(\.[\w\$:\-\#]*[^\.])*$/),
      found = false, s = sel, parts = [], pfns = [], i = 0, retStr;

    if(m === null){
      // check if literal
      if(/\'|\"/.test( s.charAt(0) )){
        if(/\'|\"/.test( s.charAt(s.length-1) )){
          retStr = s.substring(1, s.length-1);
          return function(){ return retStr; };
        }
      }else{
        // check if literal + #{var}
        while((m = s.match(/#\{([^{}]+)\}/)) !== null){
          found = true;
          parts[i++] = s.slice(0, m.index);
          pfns[i] = dataselectfn(m[1]);
          s = s.slice(m.index + m[0].length, s.length);
        }
      }
      if(!found){ //constant, return it
        return function(){ return sel; };
      }
      parts[i] = s;
      return concatenator(parts, pfns);
    }
    m = sel.split('.');
    return function(ctxt){
      var data = ctxt.context || ctxt,
        v = ctxt[m[0]],
        i = 0,
        n,
        dm;

      if(v && typeof v.item !== 'undefined'){
        i += 1;
        if(m[i] === 'pos'){
          //allow pos to be kept by string. Tx to Adam Freidin
          return v.pos;
        }
        data = v.item;
      }
      n = m.length;
        
      while( i < n ){
        if(!data){break;}
        dm = data[ m[i] ];
        //if it is a function call it
        data = typeof dm === 'function' ? dm.call( data ) : dm;
        i++;
      }
      
      return (!data && data !== 0) ? '':data;
    };
  }

  // wrap in an object the target node/attr and their properties
  function gettarget(dom, sel, isloop){
    var osel, prepend, selector, attr, append, target = [], m,
      setstr, getstr, quotefn, isStyle, isClass, attName, setfn;
    if( typeof sel === 'string' ){
      osel = sel;
      m = sel.match(selRx);
      if( !m ){
        error( 'bad selector syntax: ' + sel );
      }

      prepend = m[1];
      selector = m[2];
      attr = m[3];
      append = m[4];

      if(selector === '.' || ( !selector && attr ) ){
        target[0] = dom;
      }else{
        target = plugins.find(dom, selector);
      }
      if(!target || target.length === 0){
        return error('The node "' + sel + '" was not found in the template:\n' + outerHTML(dom).replace(/\t/g,'  '));
      }
    }else{
      // autoRender node
      prepend = sel.prepend;
      attr = sel.attr;
      append = sel.append;
      target = [dom];
    }

    if( prepend || append ){
      if( prepend && append ){
        error('append/prepend cannot take place at the same time');
      }else if( isloop ){
        error('no append/prepend/replace modifiers allowed for loop target');
      }else if( append && isloop ){
        error('cannot append with loop (sel: ' + osel + ')');
      }
    }
    
    if(attr){
      isStyle = (/^style$/i).test(attr);
      isClass = (/^class$/i).test(attr);
      attName = isClass ? 'className' : attr;
      setstr = function(node, s) {
        node.setAttribute(attPfx + attr, s);
        if ( node[attName] && !isStyle) {
          try{node[attName] = '';}catch(e){} //FF4 gives an error sometimes
        }
        if (node.nodeType === 1) {
          node.removeAttribute(attr);
          if(isClass){
            node.removeAttribute(attName);
          }
        }
      };
      if (isStyle || isClass) {//IE no quotes special care
        if(isStyle){
          getstr = function(n){ return n.style.cssText; };
        }else{
          getstr = function(n){ return n.className; };
        }
      }else {
        getstr = function(n){ return n.getAttribute(attr); };
      }
      quotefn = function(s){ return s.replace(/\"/g, '&quot;'); };
      if(prepend){
        setfn = function(node, s){ setstr( node, s + getstr( node )); };
      }else if(append){
        setfn = function(node, s){ setstr( node, getstr( node ) + s); };
      }else{
        setfn = function(node, s){ setstr( node, s ); };
      }
    }else{
      if (isloop) {
        setfn = function(node, s) {
          var pn = node.parentNode;
          if (pn) {
            //replace node with s
            pn.insertBefore(document.createTextNode(s), node.nextSibling);
            pn.removeChild(node);
          }else{
            error('The template root, can\'t be looped.');
          }
        };
      } else {
        if (prepend) {
          setfn = function(node, s) { node.insertBefore(document.createTextNode(s), node.firstChild); };
        } else if (append) {
          setfn = function(node, s) { node.appendChild(document.createTextNode(s));};
        } else {
          setfn = function(node, s) {
            while (node.firstChild) { node.removeChild(node.firstChild); }
            node.appendChild(document.createTextNode(s));
          };
        }
      }
      quotefn = function(s) { return s; };
    }
    return { attr: attr, nodes: target, set: setfn, sel: osel, quotefn: quotefn };
  }

  function setsig(target, n){
    var sig = Sig + n + ':', i;
    for(i = 0; i < target.nodes.length; i++){
      // could check for overlapping targets here.
      target.set( target.nodes[i], sig );
    }
  }

  // read de loop data, and pass it to the inner rendering function
  function loopfn(name, dselect, inner, sorter, filter){
    return function(ctxt){
      var a = dselect(ctxt),
        old = ctxt[name],
        temp = { items : a },
        filtered = 0,
        length,
        strs = [],
        buildArg = function(idx, temp, ftr, len){
          //keep the current loop. Tx to Adam Freidin
          var save_pos = ctxt.pos,
            save_item = ctxt.item,
            save_items = ctxt.items;
          ctxt.pos = temp.pos = idx;
          ctxt.item = temp.item = a[ idx ];
          ctxt.items = a;
          //if array, set a length property - filtered items
          if(typeof len !== 'undefined'){ (ctxt.length = len); }
          //if filter directive
          if(typeof ftr === 'function' && ftr.call(ctxt.item, ctxt) === false){
            filtered++;
            return;
          }
          strs.push( inner.call(ctxt.item, ctxt ) );
          //restore the current loop
          ctxt.pos = save_pos;
          ctxt.item = save_item;
          ctxt.items = save_items;
        },
        prop, i, ii;
      ctxt[name] = temp;
      if( isArray(a) ){
        length = a.length || 0;
        // if sort directive
        if(typeof sorter === 'function'){
          a.sort(function(a, b){
            return sorter.call(ctxt, a, b);
          });
        }
        //loop on array
        for(i = 0, ii = length; i < ii; i++){
          buildArg(i, temp, filter, length - filtered);
        }
      }else{
        if(a && typeof sorter !== 'undefined'){
          error('sort is only available on arrays, not objects');
        }
        //loop on collections
        for( prop in a ){
          if( a.hasOwnProperty( prop ) ){
            buildArg(prop, temp, filter);
          }
        }
      }

      if( typeof old !== 'undefined'){
        ctxt[name] = old;
      }else{
        delete ctxt[name];
      }
      return strs.join('');
    };
  }
  // generate the template for a loop node
  function loopgen(dom, sel, loop, fns){
    var already = false, ls, sorter, filter, prop, dsel, spec, itersel, target, nodes, node, inner;
    for(prop in loop){
      if(loop.hasOwnProperty(prop)){
        if(prop === 'sort'){
          sorter = loop.sort;
        }else if(prop === 'filter'){
          filter = loop.filter;
        }else if(already){
          error('cannot have more than one loop on a target');
        }else{
          ls = prop;
          already = true;
        }
      }
    }
    if(!ls){
      error('Error in the selector: ' + sel + '\nA directive action must be a string, a function or a loop(<-)');
    }
    dsel = loop[ls];
    // if it's a simple data selector then we default to contents, not replacement.
    if(typeof(dsel) === 'string' || typeof(dsel) === 'function'){
      loop = {};
      loop[ls] = {root: dsel};
      return loopgen(dom, sel, loop, fns);
    }
    
    spec = parseloopspec(ls);
    itersel = dataselectfn(spec.sel);
    target = gettarget(dom, sel, true);
    nodes = target.nodes;

    for(i = 0; i < nodes.length; i++){
      node = nodes[i];
      inner = compiler(node, dsel);
      fns[fns.length] = wrapquote(target.quotefn, loopfn(spec.name, itersel, inner, sorter, filter));
      target.nodes = [node];    // N.B. side effect on target.
      setsig(target, fns.length - 1);
    }
    return target;
  }

  function getAutoNodes(n, data){
    var ns = n.getElementsByTagName('*'),
      an = [],
      openLoops = {a:[],l:{}},
      cspec,
      isNodeValue,
      i, ii, j, jj, ni, cs, cj;
    //for each node found in the template
    for(i = -1, ii = ns.length; i < ii; i++){
      ni = i > -1 ?ns[i]:n;
      if(ni.nodeType === 1 && ni.className !== ''){
        //when a className is found
        cs = ni.className.split(' ');
        // for each className
        for(j = 0, jj=cs.length;j<jj;j++){
          cj = cs[j];
          // check if it is related to a context property
          cspec = checkClass(cj, ni.tagName);
          // if so, store the node, plus the type of data
          if(cspec !== false){
            isNodeValue = (/nodevalue/i).test(cspec.attr);
            if(cspec.sel.indexOf('@') > -1 || isNodeValue){
              ni.className = ni.className.replace('@'+cspec.attr, '');
              if(isNodeValue){
                cspec.attr = false;
              }
            }
            an.push({n:ni, cspec:cspec});
          }
        }
      }
    }

    function checkClass(c, tagName){
      // read the class
      var ca = c.match(selRx),
        attr = ca[3] || autoAttr[tagName],
        cspec = {prepend:!!ca[1], prop:ca[2], attr:attr, append:!!ca[4], sel:c},
        i, ii, loopi, loopil, val;
      // check in existing open loops
      for(i = openLoops.a.length-1; i >= 0; i--){
        loopi = openLoops.a[i];
        loopil = loopi.l[0];
        val = loopil && loopil[cspec.prop];
        if(typeof val !== 'undefined'){
          cspec.prop = loopi.p + '.' + cspec.prop;
          if(openLoops.l[cspec.prop] === true){
            val = val[0];
          }
          break;
        }
      }
      // not found check first level of data
      if(typeof val === 'undefined'){
        val = dataselectfn(cspec.prop)(isArray(data) ? data[0] : data);
        // nothing found return
        if(val === ''){
          return false;
        }
      }
      // set the spec for autoNode
      if(isArray(val)){
        openLoops.a.push( {l:val, p:cspec.prop} );
        openLoops.l[cspec.prop] = true;
        cspec.t = 'loop';
      }else{
        cspec.t = 'str';
      }
      return cspec;
    }

    return an;

  }

  // returns a function that, given a context argument,
  // will render the template defined by dom and directive.
  function compiler(dom, directive, data, ans){
    var fns = [], j, jj, cspec, n, target, nodes, itersel, node, inner, dsel, sels, sel, sl, i, h, parts,  pfns = [], p;
    // autoRendering nodes parsing -> auto-nodes
    ans = ans || (data && getAutoNodes(dom, data));
    if(data){
      // for each auto-nodes
      while(ans.length > 0){
        cspec = ans[0].cspec;
        n = ans[0].n;
        ans.splice(0, 1);
        if(cspec.t === 'str'){
          // if the target is a value
          target = gettarget(n, cspec, false);
          setsig(target, fns.length);
          fns[fns.length] = wrapquote(target.quotefn, dataselectfn(cspec.prop));
        }else{
          // if the target is a loop
          itersel = dataselectfn(cspec.sel);
          target = gettarget(n, cspec, true);
          nodes = target.nodes;
          for(j = 0, jj = nodes.length; j < jj; j++){
            node = nodes[j];
            inner = compiler(node, false, data, ans);
            fns[fns.length] = wrapquote(target.quotefn, loopfn(cspec.sel, itersel, inner));
            target.nodes = [node];
            setsig(target, fns.length - 1);
          }
        }
      }
    }
    // read directives
    for(sel in directive){
      if(directive.hasOwnProperty(sel)){
        i = 0;
        dsel = directive[sel];
        sels = sel.split(/\s*,\s*/); //allow selector separation by quotes
        sl = sels.length;
        do{
          if(typeof(dsel) === 'function' || typeof(dsel) === 'string'){
            // set the value for the node/attr
            sel = sels[i];
            target = gettarget(dom, sel, false);
            setsig(target, fns.length);
            fns[fns.length] = wrapquote(target.quotefn, dataselectfn(dsel));
          }else{
            // loop on node
            loopgen(dom, sel, dsel, fns);
          }
        }while(++i < sl);
      }
    }
    // convert node to a string
    h = outerHTML(dom);
      // IE adds an unremovable "selected, value" attribute
      // hard replace while waiting for a better solution
    h = h.replace(/<([^>]+)\s(value\=""|selected)\s?([^>]*)>/ig, "<$1 $3>");

    // remove attribute prefix
    h = h.split(attPfx).join('');

    // slice the html string at "Sig"
    parts = h.split( Sig );
    // for each slice add the return string of
    for(i = 1; i < parts.length; i++){
      p = parts[i];
      // part is of the form "fn-number:..." as placed there by setsig.
      pfns[i] = fns[ parseInt(p, 10) ];
      parts[i] = p.substring( p.indexOf(':') + 1 );
    }
    return concatenator(parts, pfns);
  }
  // compile the template with directive
  // if a context is passed, the autoRendering is triggered automatically
  // return a function waiting the data as argument
  function compile(directive, ctxt, template){
    var rfn = compiler( ( template || this[0] ).cloneNode(true), directive, ctxt);
    return function(context){
      return rfn({context:context});
    };
  }
  //compile with the directive as argument
  // run the template function on the context argument
  // return an HTML string
  // should replace the template and return this
  function render(ctxt, directive){
    var fn = typeof directive === 'function' && directive, i, ii;
    for(i = 0, ii = this.length; i < ii; i++){
      this[i] = replaceWith( this[i], (fn || plugins.compile( directive, false, this[i] ))( ctxt, false ));
    }
    return this;
  }

  // compile the template with autoRender
  // run the template function on the context argument
  // return an HTML string
  function autoRender(ctxt, directive){
    var fn = plugins.compile( directive, ctxt, this[0] ), i, ii;
    for(i = 0, ii = this.length; i < ii; i++){
      this[i] = replaceWith( this[i], fn( ctxt, false));
    }
    return this;
  }

  function replaceWith(elm, html) {
    var ne,
      ep = elm.parentNode,
      depth = 0,
      tmp;
    if(!ep){ //if no parents
      ep = document.createElement('DIV');
      ep.appendChild(elm);
    }
    switch (elm.tagName) {
      case 'BODY': //thanks to milan.adamovsky@gmail.com
        ep.removeChild(elm);
        ep.innerHTML += html;
        return ep.getElementsByTagName('BODY')[0];
      case 'TBODY': case 'THEAD': case 'TFOOT':
        html = '<TABLE>' + html + '</TABLE>';
        depth = 1;
      break;
      case 'TR':
        html = '<TABLE><TBODY>' + html + '</TBODY></TABLE>';
        depth = 2;
      break;
      case 'TD': case 'TH':
        html = '<TABLE><TBODY><TR>' + html + '</TR></TBODY></TABLE>';
        depth = 3;
      break;
      case 'OPTGROUP': case 'OPTION':
        html = '<SELECT>' + html + '</SELECT>';
        depth = 1;
      break;
    }
    tmp = document.createElement('SPAN');
    tmp.style.display = 'none';
    document.body.appendChild(tmp);
    tmp.innerHTML = html;
    ne = tmp.firstChild;
    while (depth--) {
      ne = ne.firstChild;
    }
    ep.insertBefore(ne, elm);
    ep.removeChild(elm);
    document.body.removeChild(tmp);
    elm = ne;

    ne = ep = null;
    return elm;
  }

  return plugins;
};

$p.plugins = {};

$p.libs = {
  dojo:function(){
    return function(n, sel){
      return dojo.query(sel, n);
    };
  },
  domassistant:function(){
    DOMAssistant.attach({
      publicMethods : [ 'compile', 'render', 'autoRender'],
      compile:function(directive, ctxt){
        return $p([this]).compile(directive, ctxt);
      },
      render:function(ctxt, directive){
        return $( $p([this]).render(ctxt, directive) )[0];
      },
      autoRender:function(ctxt, directive){
        return $( $p([this]).autoRender(ctxt, directive) )[0];
      }
    });
    return function(n, sel){
      return $(n).cssSelect(sel);
    };
  },
  ext:function(){//Thanks to Greg Steirer
    return function(n, sel){
      return Ext.query(sel, n);
    };
  },
  jquery:function(){
    jQuery.fn.extend({
      directives:function(directive){
        this._pure_d = directive; return this;
      },
      compile:function(directive, ctxt){
        return $p(this).compile(this._pure_d || directive, ctxt);
      },
      render:function(ctxt, directive){
        return jQuery( $p( this ).render( ctxt, this._pure_d || directive ) );
      },
      autoRender:function(ctxt, directive){
        return jQuery( $p( this ).autoRender( ctxt, this._pure_d || directive ) );
      }
    });
    return function(n, sel){
      return jQuery(n).find(sel);
    };
  },
  mootools:function(){
    Element.implement({
      compile:function(directive, ctxt){
        return $p(this).compile(directive, ctxt);
      },
      render:function(ctxt, directive){
        return $p([this]).render(ctxt, directive);
      },
      autoRender:function(ctxt, directive){
        return $p([this]).autoRender(ctxt, directive);
      }
    });
    return function(n, sel){
      return $(n).getElements(sel);
    };
  },
  prototype:function(){
    Element.addMethods({
      compile:function(element, directive, ctxt){
        return $p([element]).compile(directive, ctxt);
      },
      render:function(element, ctxt, directive){
        return $p([element]).render(ctxt, directive);
      },
      autoRender:function(element, ctxt, directive){
        return $p([element]).autoRender(ctxt, directive);
      }
    });
    return function(n, sel){
      n = n === document ? n.body : n;
      return typeof n === 'string' ? $$(n) : $(n).select(sel);
    };
  },
  sizzle:function(){
    return function(n, sel){
      return Sizzle(sel, n);
    };
  },
  sly:function(){
    return function(n, sel){
      return Sly(sel, n);
    };
  },
  yui:function(){ //Thanks to https://github.com/soljin
    if(typeof document.querySelector === 'undefined'){
      YUI().use("node",function(Y){
        $p.plugins.find = function(n, sel){
          return Y.NodeList.getDOMNodes(Y.one(n).all(sel));
        };
      });
    }
    YUI.add("pure-yui",function(Y){
      Y.Node.prototype.directives = function(directive){
        this._pure_d = directive; return this;
      };
      Y.Node.prototype.compile = function(directive, ctxt){
        return $p([this._node]).compile(this._pure_d || directive, ctxt);
      };
      Y.Node.prototype.render = function(ctxt, directive){
        return Y.one($p([this._node]).render(ctxt, this._pure_d || directive));
      };
      Y.Node.prototype.autoRender = function(ctxt, directive){
        return Y.one($p([this._node]).autoRender(ctxt, this._pure_d || directive));
      };
    },"0.1",{requires:["node"]});

    return true;
  }
};

// get lib specifics if available
(function(){
  var libSel,
    libkey =
      (typeof dojo         !== 'undefined' && 'dojo') ||
      (typeof DOMAssistant !== 'undefined' && 'domassistant') ||
      (typeof Ext          !== 'undefined' && 'ext') ||
      (typeof jQuery       !== 'undefined' && 'jquery') ||
      (typeof MooTools     !== 'undefined' && 'mootools') ||
      (typeof Prototype    !== 'undefined' && 'prototype') ||
      (typeof Sizzle       !== 'undefined' && 'sizzle') ||
      (typeof Sly          !== 'undefined' && 'sly') ||
      (typeof YUI          !== 'undefined' && 'yui');
  
  //add library methods
  if(libkey){
    libSel = $p.libs[libkey]();
  }
  
  //if no native selector available
  if( typeof document.querySelector === 'undefined' ){
    //take it from the JS lib
    if( typeof libSel === 'function' ){
      $p.plugins.find = libSel;
    //if nothing throw an error
    }else if( !libSel ){
      throw('you need a JS library with a CSS selector engine');
    }
  }

  //for node.js
  if(typeof exports !== 'undefined'){
    exports.$p = $p;
  }
}());

</script>
<div class="pure">
        <h1 class='header'></h1>
        <h2 class='header2'></h2>
        <h3 class='header3'></h3>
        <h4 class='header4'></h4>
        <h5 class='header5'></h5>
        <h6 class='header6'></h6>
        <ul class='list'>
                <li class='item'></li>
        </ul>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/json2/20130526/json2.min.js"></script>

<!--<script src="http://akdubya.github.com/dustjs/dist/dust-full-0.3.0.min.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/dustjs-linkedin/2.0.0/dust-core.min.js"></script>

<!-- https://github.com/premasagar/tim/raw/master/tim.js -->
<script>
/*!
* Tim
*   github.com/premasagar/tim
*
*//*
    A tiny, secure JavaScript micro-templating script.
*//*

    by Premasagar Rose
        dharmafly.com

    license
        opensource.org/licenses/mit-license.php

    **

    creates global object
        tim

    **

    v0.3.0

*//*global window */

/*
    TODO:
    * a way to prevent a delimiter (e.g. ", ") appearing last in a loop template
    * Sorted constructor for auto-sorting arrays - used for parsers -> two parsers are added, one for identifying and parsing single-tokens and one for open/close tokens - the parsers then create two new Sorted instance, one for single-token plugins and one for open/close token plugins
*/

(function(name, definition, context) {
    if (typeof module != 'undefined' && module.exports) {
        module.exports = definition();
    } else if (typeof context['define'] == 'function' && context['define']['amd']) {
        define(definition);
    } else {
        context[name] = definition();
    }
})('tim', function() {

    var tim = (function createTim(initSettings){
        "use strict";
        
        var settings = {
                start: "{{",
                end  : "}}",
                path : "[a-z0-9_$][\\.a-z0-9_]*" // e.g. config.person.name
            },
            templates = {},
            filters = {},
            stopThisFilter, pattern, initialized, undef;
            
            
        /////
        

        // Update cached regex pattern
        function patternCache(){
            pattern = new RegExp(settings.start + "\\s*("+ settings.path +")\\s*" + settings.end, "gi");
        }
        
        // settingsCache: Get and set settings
        /*
            Example usage:
            settingsCache(); // get settings object
            settingsCache({start:"<%", end:"%>", attr:"id"}); // set new settings
        */
        function settingsCache(newSettings){
            var s;
        
            if (newSettings){
                for (s in newSettings){
                    if (newSettings.hasOwnProperty(s)){
                        settings[s] = newSettings[s];
                    }
                }
                patternCache();
            }
            return settings;
        }
            
        // Apply custom settings
        if (initSettings){
            settingsCache(initSettings);
        }
        else {
            patternCache();
        }
        
        
        /////
        
        
        // templatesCache: Get and set the templates cache object
        /*
            Example usage:
            templatesCache("foo"); // get template named "foo"
            templatesCache("foo", "bar"); // set template named "foo" to "bar"
            templatesCache("foo", false); // delete template named "foo"
            templatesCache({foo:"bar", blah:false}); // set multiple templates
            templatesCache(false); // delete all templates
        */
        function templatesCache(key, value){
            var t;
        
            switch (typeof key){
                case "string":
                    if (value === undef){
                        return templates[key] || "";
                    }
                    else if (value === false){
                        delete templates[key];
                    }
                    else {
                        templates[key] = value;
                    }
                break;
                
                case "object":
                    for (t in key){
                        if (key.hasOwnProperty(t)){
                            templatesCache(t, key[t]);
                        }
                    }
                break;
                
                case "boolean":
                if (!key){
                    templates = {};
                }
                break;
            }
            return templates;
        }
        
        function extend(obj1, obj2){
            var key;
            for (key in obj2){
                if (obj2.hasOwnProperty(key)){
                    obj1[key] = obj2[key];
                }
            }
            return obj1;
        }
        
        
        /////
        
        
        // FILTERS    
        function sortByPriority(a, b){
            return a[1] - b[1];
        }
        
        // Add filter to the stack
        function addFilter(filterName, fn, priority){
            var fns = filters[filterName];
            if (!fns){
                fns = filters[filterName] = [];
            }
            fns.push([fn, priority || 0]);
            fns.sort(sortByPriority);
            return fn;
        }
        
        function applyFilter(filterName, payload){
            var fns = filters[filterName],
                args, i, len, substituted;
                
            if (fns){
                args = [payload];
                i = 2;
                len = arguments.length;            
                for (; i < len; i++){
                    args.push(arguments[i]);
                }

                i = 0;
                len = fns.length;
                for (; i < len; i++){
                    args[0] = payload;
                    substituted = fns[i][0].apply(null, args);
                    if (payload !== undef && substituted !== undef){
                        payload = substituted;
                    }
                    if (substituted === null){
                        payload = '';
                    }
                    if (stopThisFilter){
                        stopThisFilter = false;
                        break;
                    }
                }
            }
            return payload;
        }
        
        // Router for adding and applying filters, for Tim API
        function filter(filterName, payload){
            return (typeof payload === "function" ? addFilter : applyFilter)
                .apply(null, arguments);
        }
        filter.stop = function(){
            stopThisFilter = true;
        };
        
        
        /////
        
        
        // Merge data into template
        /*  
            // simpler alternative, without support for iteration:
            template = template.replace(pattern, function(tag, token){
                return applyFilter("token", token, data, template);
            });
        */
        // TODO: all an array to be passed to tim(), so that the template is called for each element in it
        function substitute(template, data){
            var match, tag, token, substituted, startPos, endPos, templateStart, templateEnd, subTemplate, closeToken, closePos, key, loopData, loop;
        
            while((match = pattern.exec(template)) !== null) {
                token = match[1];
                substituted = applyFilter("token", token, data, template);
                startPos = match.index;
                endPos = pattern.lastIndex;
                templateStart = template.slice(0, startPos);
                templateEnd = template.slice(endPos);
                
                // If the final value is a function call it and use the returned
                // value in its place.
                if (typeof substituted === "function") {
                    substituted = substituted.call(data);
                }
                
                if (typeof substituted !== "boolean" && typeof substituted !== "object"){
                    template = templateStart + substituted + templateEnd;
                } else {
                    subTemplate = "";
                    closeToken = settings.start + "/" + token + settings.end;
                    closePos = templateEnd.indexOf(closeToken);
                    
                    if (closePos >= 0){
                        templateEnd = templateEnd.slice(0, closePos);
                        if (typeof substituted === "boolean") {
                            subTemplate = substituted ? templateEnd : '';
                        } else {
                            for (key in substituted){
                                if (substituted.hasOwnProperty(key)){
                                    pattern.lastIndex = 0;
                                
                                    // Allow {{_key}} and {{_content}} in templates
                                    loopData = extend({_key:key, _content:substituted[key]}, substituted[key]);
                                    loopData = applyFilter("loopData", loopData, loop, token);
                                    loop = tim(templateEnd, loopData);
                                    subTemplate += applyFilter("loop", loop, token, loopData);
                                }
                            }
                            subTemplate = applyFilter("loopEnd", subTemplate, token, loopData);
                        }
                        template = templateStart + subTemplate + template.slice(endPos + templateEnd.length + closeToken.length);
                    }
                    else {
                        throw "tim: '" + token + "' not closed";
                    }
                }
                
                pattern.lastIndex = 0;
            }
            return template;
        }
        
        
        // TIM - MAIN FUNCTION
        function tim(template, data){
            var templateLookup;
        
            // On first run, call init plugins
            if (!initialized){
                initialized = 1;        
                applyFilter("init");
            }
            template = applyFilter("templateBefore", template);
        
            // No template tags found in template
            if (template.indexOf(settings.start) < 0){
                // Is this a key for a cached template?
                templateLookup = templatesCache(template);
                if (templateLookup){
                    template = templateLookup;
                }
            }
            template = applyFilter("template", template);
            
            // Substitute tokens in template
            if (template && data !== undef){
                template = substitute(template, data);
            }
            
            template = applyFilter("templateAfter", template);
            return template;
        }
        
        // Get and set settings, e.g. tim({attr:"id"});
        tim.settings = settingsCache;
        
        // Get and set cached templates
        tim.templates = templatesCache;
        
        // Create new Tim function, based on supplied settings, if any
        tim.parser = createTim;
        
        // Add new filters and trigger existing ones. Use tim.filter.stop() during processing, if required.
        tim.filter = filter;
        
        
        /////
        
        
        // dotSyntax default plugin: uses dot syntax to parse a data object for substitutions
        addFilter("token", function(token, data, tag){
            var path = token.split("."),
                len = path.length,
                dataLookup = data,
                i = 0;

            for (; i < len; i++){
                dataLookup = dataLookup[path[i]];
                
                // Property not found
                if (dataLookup === undef){
                    throw "tim: '" + path[i] + "' not found" + (i ? " in " + tag : "");
                }
                
                // Return the required value
                if (i === len - 1){
                    return dataLookup;
                }
            }
        });
        
        
        /////
        
        
        // Dom plugin: finds micro-templates in <script>'s in the DOM
        // This block of code can be removed if unneeded - e.g. with server-side JS
        // Default: <script type="text/tim" class="foo">{{TEMPLATE}}<\/script>
        if (window && window.document){
            tim.dom = function(domSettings){
                domSettings = domSettings || {};
                
                var type = domSettings.type || settings.type || "text/tim",
                    attr = domSettings.attr || settings.attr || "class",
                    document = window.document,
                    hasQuery = !!document.querySelectorAll,
                    elements = hasQuery ?
                        document.querySelectorAll(
                            "script[type='" + type + "']"
                        ) :
                        document.getElementsByTagName("script"),
                    i = 0,
                    len = elements.length,
                    elem, key,
                    templatesInDom = {};
                    
                for (; i < len; i++){
                    elem = elements[i];
                    // Cannot access "class" using el.getAttribute()
                    key = attr === "class" ? elem.className : elem.getAttribute(attr);
                    if (key && (hasQuery || elem.type === type)){
                        templatesInDom[key] = elem.innerHTML;
                    }
                }
                
                templatesCache(templatesInDom);
                return templatesInDom;
            };
            
            addFilter("init", function(){
                tim.dom();
            });
        }
        
        return tim;
    }());

    return tim;

}, this);

/*jslint browser: true, onevar: true, undef: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: true */
</script>
<!-- https://gist.github.com/raw/875670/d52752ead19a4eebc7237602438ae08a2541a5b5/tim-lite-cached-min.js -->
<script>
  var timLiteCached=function(){function i(b,e){return b.replace(m,function(g,c){for(var j=c.split("."),k=j.length,h=e,f=0;f<k;f++){h=h[j[f]];if(f===k-1)return h}})}var m=RegExp("{{\\s*([a-z0-9_][\\.a-z0-9_]*)\\s*}}","gi"),d={},l=window.JSON;return l?function(b,e){var g=l.stringify(e),c=d[b]&&d[b][g];if(c)return c;d[b]||(d[b]={});return c=d[b][g]=i(b,e)}:i}();
</script>
<script src="http://www.kuwata-lab.com/tenjin/shotenjin.js"></script>

<!-- https://github.com/QLeelulu/nTenjin/raw/master/nTenjin.js -->
<script>
/*
 * $Rev: 39 $
 * $Release: 0.0.0 $
 * $Copyright$
 * License:  MIT License
 */

/**
 *  namespace
 */

var nTenjin = {
  
  version: '0.1.1', 
  
  _escape_table: { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' },

  _escape_func: function(m) { return nTenjin._escape_table[m]; },

  escapeXml: function(s) {
    //if (s == null) return '';
    return typeof(s) != 'string' ? s : s.replace(/[&<>"]/g, nTenjin._escape_func); //"
    //return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); //"
  },

  escapeXml2: function(s) {
    if (s == null) return '';
    return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');  //"
  },

  strip: function(s) {
    if (! s) return s;
    //return s.replace(/^\s+|\s+$/g, '');
    return s.replace(/^\s+/, '').replace(/\s+$/, '');
  },
  
  _end: undefined  // dummy property to escape strict warning (not legal in ECMA-262)
};
delete(nTenjin._end);

// 因为用 new Function 创建的函数,其[[scope]]的作用域链只包含全局对象,所以这里定义为全局变量
_nTenjinEscapeXml = nTenjin.escapeXml;


/**
 *  Template class
 */

nTenjin.Template = function(properties) {
  if (properties) {
    var p = properties;
    if (p['escaefunc']) this.escapefunc = p['escapefunc'];
  }
};

nTenjin.Template.prototype = {

  escapefunc: '_nTenjinEscapeXml',

  convert: function(input) {
    var buf = [];
    buf.push("var _buf='';");
    this.parseStatements(buf, input);
    buf.push("return _buf;");
    buf = buf.join('').split("_buf+='';").join('')
       .split("var _buf='';_buf+=").join('var _buf=');
        try {
      return this.render = new Function('it', buf);
            //eval('this.render = function(it){' + buf + '};');
            //return this.render;
    } catch (e) {
      if (typeof console !== 'undefined') console.log("Could not create a template function: " + buf);
      throw e;
    }
  },

  parseStatements: function(buf, input) {
    var regexp = /<\?js(\s(.|\n)*?) ?\?>/mg;
    var pos = 0;
    var m;
    while ((m = regexp.exec(input)) != null) {
      var stmt = m[1];
      var text = input.substring(pos, m.index);
      pos = m.index + m[0].length;
      //
      if (text) this.parseExpressions(buf, text);
      if (stmt) buf.push(stmt);
    }
    var rest = pos == 0 ? input : input.substring(pos);
    this.parseExpressions(buf, rest);
  },

  parseExpressions: function(buf, input) {
    if (! input) return;
    //buf.push(" _buf+=");
    var regexp = /([$#])\{(.*?)\}/g;
    var pos = 0;
    var m;
    while ((m = regexp.exec(input)) != null) {
      var text = input.substring(pos, m.index);
      var s = m[0];
      pos = m.index + s.length;
      this.addText(buf, text);
      buf.push(";_buf+=");
      var indicator = m[1];
      var expr = m[2];
      if (indicator == "$")
        buf.push(this.escapefunc, "(", expr, ");");
      else
        buf.push(expr, ";");
    }
    var rest = pos == 0 ? input : input.substring(pos);
    rest ? this.addText(buf, rest, true) : buf.push('""');
    buf.push(";");
    if (input.charAt(input.length-1) == "\n")
      buf.push("\n");
  },

  addText: function(buf, text, encode_newline) {
    if (! text) return;
    var s = text.replace(/[\'\\]/g, '\\$&').replace(/\n/g, '\\n\\\n');
    buf.push("_buf+='", s, "'");
  },

  _end: undefined  // dummy property to escape strict warning (not legal in ECMA-262)
};
delete(nTenjin.Template.prototype._end);


/*
 *  convenient function
 */
nTenjin.render = function(template_str, context) {
  var template = new nTenjin.Template();
  template.convert(template_str);
  var output = template.render(context);
  return output;
};

/**
 * compile str to Function
 * 
 * @param {String} str, template string
 * @return {Function}
 * @api public
 */

nTenjin.compile = function(str, options) {
  var tpl = new nTenjin.Template();
  return tpl.convert(str);
};

/*
 *  node.js
 */
if (typeof module !== 'undefined' && module.exports) {
  module.exports = nTenjin;
}


</script>
<script>
  window.mustacheTemplate = "<div><h1 class='header'>{{header}}</h1><h2 class='header2'>{{header2}}</h2><h3 class='header3'>{{header3}}</h3><h4 class='header4'>{{header4}}</h4><h5 class='header5'>{{header5}}</h5><h6 class='header6'>{{header6}}</h6><ul class='list'>{{#list}}<li class='item'>{{.}}</li>{{/list}}</ul></div>";
  
  // note: exactly the same as the mustacheTemplate above.
  window.kiteTemplate = "<div><h1 class='header'>{{header}}</h1><h2 class='header2'>{{header2}}</h2><h3 class='header3'>{{header3}}</h3><h4 class='header4'>{{header4}}</h4><h5 class='header5'>{{header5}}</h5><h6 class='header6'>{{header6}}</h6><ul class='list'>{{#list}}<li class='item'>{{.}}</li>{{/list}}</ul></div>";
  window.kiteCompiledTemplate = kite(kiteTemplate); // seems like others are testing compiled versions.
  window.handlebarsTemplate = Handlebars.compile("<div><h1 class='header'>{{header}}</h1><h2 class='header2'>{{header2}}</h2><h3 class='header3'>{{header3}}</h3><h4 class='header4'>{{header4}}</h4><h5 class='header5'>{{header5}}</h5><h6 class='header6'>{{header6}}</h6><ul class='list'>{{#each list}}<li class='item'>{{this}}</li>{{/each}}</ul></div>");
  
  window.micro = "<div><h1 class='header'><%= header %></h1><h2 class='header2'><%= header2 %></h2><h3 class='header3'><%= header3 %></h3><h4 class='header4'><%= header4 %></h4><h5 class='header5'><%= header5 %></h5><h6 class='header6'><%= header6 %></h6><ul class='list'><% for (var i = 0, l = list.length; i < l; i++) { %><li class='item'><%= list[i] %></li><% } %></ul></div>";
  
  window.micro2 = "<div><h1 class='header'><%= data.header %></h1><h2 class='header2'><%= data.header2 %></h2><h3 class='header3'><%= data.header3 %></h3><h4 class='header4'><%= data.header4 %></h4><h5 class='header5'><%= data.header5 %></h5><h6 class='header6'><%= data.header6 %></h6><ul class='list'><% for (var i = 0, l = data.list.length; i < l; i++) { %><li class='item'><%= data.list[i] %></li><% } %></ul></div>";
  
  
  window.underscoreTemplate = _.template(micro);
  
  window.resigTemplate = tmpl(micro);
  
  window.resigTemplate2 = tmpl2(micro2);
  
  window.resigTemplate3 = tmpl3(micro2);
  
  window.sharedVariables = {
   header: "Header",
   header2: "Header2",
   header3: "Header3",
   header4: "Header4",
   header5: "Header5",
   header6: "Header6",
   list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
  };
  
  window.jadeTemplate = "div\n  h1.header!= header\n  h2.header2!= header2\n  h3.header3!= header3\n  h4.header4!= header4\n  h5.header5!= header5\n  h6.header6!= header6\n  ul.list\n    - each item in list\n      li.item!= item";
  
  window.hamlTemplate = Haml("%div\n  %h1.header= header\n  %h2.header2= header2\n  %h3.header3= header3\n  %h4.header4= header4\n  %h5.header5= header5\n  %h6.header6= header6\n  %ul.list\n    :each item in list\n      %li.item= item");
  
  window.ecoTemplate = eco("<div><h1 class='header'><%- @header %></h1><h2 class='header2'><%- @header2 %></h2><h3 class='header3'><%- @header3 %></h3><h4 class='header4'><%- @header4 %></h4><h5 class='header5'><%- @header5 %></h5><h6 class='header6'><%- @header6 %></h6><ul class='list'><% for item in @list: %><li class='item'><%- item %></li><% end %></ul></div>");
  
  window.jQueryTemplate = $.template(null, "<div><h1 class='header'>{{html header}}</h1><h2 class='header2'>{{html header2}}</h2><h3 class='header3'>{{html header3}}</h3><h4 class='header4'>{{html header4}}</h4><h5 class='header5'>{{html header5}}</h5><h6 class='header6'>{{html header6}}</h6><ul class='list'>{{each list}}<li class='item'>{{html $value}}</li>{{/each}}</ul></div>");
  
  window.doTtemplate = doT.template("<div><h1 class='header'>{{= it.header }}</h1><h2 class='header2'>{{= it.header2 }}</h2><h3 class='header3'>{{= it.header3 }}</h3><h4 class='header4'>{{= it.header4 }}</h4><h5 class='header5'>{{= it.header5 }}</h5><h6 class='header6'>{{= it.header6 }}</h6><ul class='list'>{{ for (var i = 0, l = it.list.length; i < l; i++) { }}<li class='item'>{{= it.list[i] }}</li>{{ } }}</ul></div>");
  
  window.doUtemplate = doU.template("<div><h1 class='header'>{{= it.header }}</h1><h2 class='header2'>{{= it.header2 }}</h2><h3 class='header3'>{{= it.header3 }}</h3><h4 class='header4'>{{= it.header4 }}</h4><h5 class='header5'>{{= it.header5 }}</h5><h6 class='header6'>{{= it.header6 }}</h6><ul class='list'>{{ for (var i = 0, l = it.list.length; i < l; i++) { }}<li class='item'>{{= it.list[i] }}</li>{{ } }}</ul></div>");
  
  //window.jqote2 = $.jqotec(jqote_tmpl);
  
  window.ejs = new EJS({
   text: micro
  });
  
  window.baseHtml = "<div><h1 class='header'></h1><h2 class='header2'></h2><h3 class='header3'></h3><h4 class='header4'></h4><h5 class='header5'></h5><h6 class='header6'></h6><ul class='list'><li class='item'></li></ul></div>";
  
  window.pureTemplate = $p('div.pure').compile({
   h1: 'header',
   h2: 'header2',
   h3: 'header3',
   h4: 'header4',
   h5: 'header5',
   h6: 'header6',
   li: {
    'itm<-list': {
     '.': 'itm'
    }
   }
  });
  
  window.hamlTemplate_2 = "%div\n  %h1.header= header\n  %h2.header2= header2\n  %h3.header3= header3\n  %h4.header4= header4\n  %h5.header5= header5\n  %h6.header6= header6\n  %ul.list\n    :each item in list\n      %li.item= item";
  window.hamlCompiled = Haml.compile(hamlTemplate_2);
  window.hamlOptimized = Haml.optimize(hamlCompiled);
  
  dust.loadSource(dust.compile("<div><h1 class='header'>{header}</h1><h2 class='header2'>{header2}</h2><h3 class='header3'>{header3}</h3><h4 class='header4'>{header4}</h4><h5 class='header5'>{header5}</h5><h6 class='header6'>{header6}</h6><ul class='list'>{#list}<li class='item'>{.}</li>{/list}</ul></div>", "dustTemplate"));
  
  window.timTemplate = "<div><h1 class='header'>{{header}}</h1><h2 class='header2'>{{header2}}</h2><h3 class='header3'>{{header3}}</h3><h4 class='header4'>{{header4}}</h4><h5 class='header5'>{{header5}}</h5><h6 class='header6'>{{header6}}</h6><ul class='list'>{{list}}<li class='item'>{{_content}}</li>{{/list}}</ul></div>";
  
  window.timLiteTemplate = "<div><h1 class='header'>{{header}}</h1><h2 class='header2'>{{header2}}</h2><h3 class='header3'>{{header3}}</h3><h4 class='header4'>{{header4}}</h4><h5 class='header5'>{{header5}}</h5><h6 class='header6'>{{header6}}</h6><ul class='list'><li class='item'>{{list.0}}</li><li class='item'>{{list.1}}</li><li class='item'>{{list.2}}</li><li class='item'>{{list.3}}</li><li class='item'>{{list.4}}</li><li class='item'>{{list.5}}</li><li class='item'>{{list.6}}</li><li class='item'>{{list.7}}</li><li class='item'>{{list.8}}</li><li class='item'>{{list.9}}</li></ul></div>";
  
  window.tenjinTemplate = "<div><h1 class='header'>#{ header }</h1><h2 class='header2'>#{ header2 }</h2><h3 class='header3'>#{ header3 }</h3><h4 class='header4'>#{ header4 }</h4><h5 class='header5'>#{ header5 }</h5><h6 class='header6'>#{ header6 }</h6><ul class='list'><?js for (var i = 0, l = list.length; i < l; i++) { ?><li class='item'>#{ list[i] }</li><?js } ?></ul></div>";
  
  window.convertedTenjinTemplate = new Shotenjin.Template();
  window.convertedTenjinTemplate.convert(tenjinTemplate);
  
  window.nTenjinTemplate = "<div><h1 class='header'>#{ it.header }</h1><h2 class='header2'>#{ it.header2 }</h2><h3 class='header3'>#{ it.header3 }</h3><h4 class='header4'>#{ it.header4 }</h4><h5 class='header5'>#{ it.header5 }</h5><h6 class='header6'>#{ it.header6 }</h6><ul class='list'><?js for (var i = 0, l = it.list.length; i < l; i++) { ?><li class='item'>#{ it.list[i] }</li><?js } ?></ul></div>";
  
  window.convertednTenjinTemplate = new nTenjin.Template();
  window.convertednTenjinTemplate.convert(nTenjinTemplate);
</script>

Test runner

Ready to run.

Testing in
TestOps/sec
Mustache.js Template
Mustache.to_html(mustacheTemplate, sharedVariables);
ready
Resig Micro-Templating.
resigTemplate(sharedVariables);
ready
Creationix's Haml-js Template
hamlTemplate(sharedVariables);
ready
Jade Template
jade.render(jadeTemplate, {
 locals: sharedVariables,
 filename: 'test.js',
 cache: true
});
ready
Underscore.js Template
underscoreTemplate(sharedVariables);
ready
Eco Template
ecoTemplate(sharedVariables);
ready
jQuery Templates
jQueryTemplate($, {
 data: sharedVariables
}).join("");
ready
Handlebars.js
handlebarsTemplate(sharedVariables);
ready
doT.js
doTtemplate(sharedVariables);
ready
doU.js
doUtemplate(sharedVariables);
ready
ejs
ejs.render(sharedVariables);
ready
Pure
pureTemplate(sharedVariables);
ready
Creationix's Haml-js Template (compiled & optimized)
Haml.execute(window.hamlOptimized, sharedVariables, sharedVariables);
ready
dust
var done = false;

while (!done) {
 // Need better way to benchmark asynchronous functions
 dust.render("dustTemplate", sharedVariables, function() {
  done = true;
 });
}
ready
KiTE
kiteCompiledTemplate(sharedVariables);
ready
Resig Micro-Templating without with(obj)
resigTemplate2(sharedVariables);
ready
Resig Micro-Templating with += instead of push and join
resigTemplate3(sharedVariables);
ready
Milk
Milk.render(mustacheTemplate, sharedVariables);
ready
Tim
tim(timTemplate, sharedVariables);
ready
Tim (lite, cached)
timLiteCached(timLiteTemplate, sharedVariables);
ready
jsTenjin(not converted)
Shotenjin.render(tenjinTemplate, sharedVariables);
ready
jsTenjin(converted)
convertedTenjinTemplate.render(sharedVariables);
ready
nTenjin(converted)
convertednTenjinTemplate.render(sharedVariables);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.