[extensions-web] js: Import mustache, introduce a new templates system



commit c4ed8a6624d9f10dd199cd4c41098be19042b1d7
Author: Jasper St. Pierre <jstpierre mecheye net>
Date:   Fri Mar 30 17:44:25 2012 -0400

    js: Import mustache, introduce a new templates system
    
    If we want to sanely translate the website, we need to have some
    basic form of templating. Having hesitated, waiting for the jQuery
    team to develop a templating framework for the fifth time, I think
    I've waited long enough. Mustache it is.
    
    Port some basic things over to the new templates system, as a test.

 sweettooth/static/js/extensions.js                 |   19 +-
 sweettooth/static/js/mustache.js                   |  536 ++++++++++++++++++++
 sweettooth/static/js/paginator.js                  |    7 +-
 sweettooth/static/js/templates.js                  |   25 +
 sweettooth/static/js/templates/build_templates.py  |   34 ++
 .../js/templates/paginator/loading_page.mustache   |    3 +
 sweettooth/static/js/templates/templatedata.js     |    4 +
 .../js/templates/upgrade/latest_version.mustache   |    1 +
 .../js/templates/upgrade/need_upgrade.mustache     |    1 +
 9 files changed, 616 insertions(+), 14 deletions(-)
---
diff --git a/sweettooth/static/js/extensions.js b/sweettooth/static/js/extensions.js
index 4d4eeec..a8c853d 100644
--- a/sweettooth/static/js/extensions.js
+++ b/sweettooth/static/js/extensions.js
@@ -1,8 +1,8 @@
 "use strict";
 
-define(['jquery', 'messages', 'dbus!_', 'extensionUtils', 'paginator',
-        'switch', 'jquery.tipsy'],
-function($, messages, dbusProxy, extensionUtils) {
+define(['jquery', 'messages', 'dbus!_', 'extensionUtils', 'templates',
+        'paginator', 'switch', 'jquery.tipsy'],
+function($, messages, dbusProxy, extensionUtils, templates) {
 
     var ExtensionState = extensionUtils.ExtensionState;
 
@@ -392,16 +392,15 @@ function($, messages, dbusProxy, extensionUtils) {
                 if (!meta)
                     return;
 
-                if (vpk.version > meta.version) {
-                    var msg = "You have version " + meta.version + " of";
-                    msg += "\"" + extensionName + "\"";
-                    msg += ". The latest version is version " + vpk.version;
-                    msg += ". Click here to upgrade.";
+                var context = { latest_version: vpk.version,
+                                current_version: meta.version,
+                                extension_name: extensionName };
 
+                if (vpk.version > meta.version) {
+                    var msg = templates.upgrade.need_upgrade(context);
                     $upgradeMe.append($('<a>', { href: '#' }).text(msg).click(upgrade));
                 } else if (vpk.version == meta.version) {
-                    var msg = "You have the latest version of ";
-                    msg += "\"" + extensionName + "\"";
+                    var msg = templates.upgrade.latest_version(context);
                     $upgradeMe.text(msg);
                 }
             });
diff --git a/sweettooth/static/js/mustache.js b/sweettooth/static/js/mustache.js
new file mode 100644
index 0000000..641cebd
--- /dev/null
+++ b/sweettooth/static/js/mustache.js
@@ -0,0 +1,536 @@
+/*!
+ * mustache.js - Logic-less {{mustache}} templates with JavaScript
+ * http://github.com/janl/mustache.js
+ */
+var Mustache = (typeof module !== "undefined" && module.exports) || {};
+
+(function (exports) {
+
+  exports.name = "mustache.js";
+  exports.version = "0.5.0-dev";
+  exports.tags = ["{{", "}}"];
+  exports.parse = parse;
+  exports.compile = compile;
+  exports.render = render;
+  exports.clearCache = clearCache;
+
+  // This is here for backwards compatibility with 0.4.x.
+  exports.to_html = function (template, view, partials, send) {
+    var result = render(template, view, partials);
+
+    if (typeof send === "function") {
+      send(result);
+    } else {
+      return result;
+    }
+  };
+
+  var _toString = Object.prototype.toString;
+  var _isArray = Array.isArray;
+  var _forEach = Array.prototype.forEach;
+  var _trim = String.prototype.trim;
+
+  var isArray;
+  if (_isArray) {
+    isArray = _isArray;
+  } else {
+    isArray = function (obj) {
+      return _toString.call(obj) === "[object Array]";
+    };
+  }
+
+  var forEach;
+  if (_forEach) {
+    forEach = function (obj, callback, scope) {
+      return _forEach.call(obj, callback, scope);
+    };
+  } else {
+    forEach = function (obj, callback, scope) {
+      for (var i = 0, len = obj.length; i < len; ++i) {
+        callback.call(scope, obj[i], i, obj);
+      }
+    };
+  }
+
+  var spaceRe = /^\s*$/;
+
+  function isWhitespace(string) {
+    return spaceRe.test(string);
+  }
+
+  var trim;
+  if (_trim) {
+    trim = function (string) {
+      return string == null ? "" : _trim.call(string);
+    };
+  } else {
+    var trimLeft, trimRight;
+
+    if (isWhitespace("\xA0")) {
+      trimLeft = /^\s+/;
+      trimRight = /\s+$/;
+    } else {
+      // IE doesn't match non-breaking spaces with \s, thanks jQuery.
+      trimLeft = /^[\s\xA0]+/;
+      trimRight = /[\s\xA0]+$/;
+    }
+
+    trim = function (string) {
+      return string == null ? "" :
+        String(string).replace(trimLeft, "").replace(trimRight, "");
+    };
+  }
+
+  var escapeMap = {
+    "&": "&amp;",
+    "<": "&lt;",
+    ">": "&gt;",
+    '"': '&quot;',
+    "'": '&#39;'
+  };
+
+  function escapeHTML(string) {
+    return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) {
+      return escapeMap[s] || s;
+    });
+  }
+
+  /**
+   * Adds the `template`, `line`, and `file` properties to the given error
+   * object and alters the message to provide more useful debugging information.
+   */
+  function debug(e, template, line, file) {
+    file = file || "<template>";
+
+    var lines = template.split("\n"),
+        start = Math.max(line - 3, 0),
+        end = Math.min(lines.length, line + 3),
+        context = lines.slice(start, end);
+
+    var c;
+    for (var i = 0, len = context.length; i < len; ++i) {
+      c = i + start + 1;
+      context[i] = (c === line ? " >> " : "    ") + context[i];
+    }
+
+    e.template = template;
+    e.line = line;
+    e.file = file;
+    e.message = [file + ":" + line, context.join("\n"), "", e.message].join("\n");
+
+    return e;
+  }
+
+  /**
+   * Looks up the value of the given `name` in the given context `stack`.
+   */
+  function lookup(name, stack, defaultValue) {
+    if (name === ".") {
+      return stack[stack.length - 1];
+    }
+
+    var names = name.split(".");
+    var lastIndex = names.length - 1;
+    var target = names[lastIndex];
+
+    var value, context, i = stack.length, j, localStack;
+    while (i) {
+      localStack = stack.slice(0);
+      context = stack[--i];
+
+      j = 0;
+      while (j < lastIndex) {
+        context = context[names[j++]];
+
+        if (context == null) {
+          break;
+        }
+
+        localStack.push(context);
+      }
+
+      if (context && typeof context === "object" && target in context) {
+        value = context[target];
+        break;
+      }
+    }
+
+    // If the value is a function, call it in the current context.
+    if (typeof value === "function") {
+      value = value.call(localStack[localStack.length - 1]);
+    }
+
+    if (value == null)  {
+      return defaultValue;
+    }
+
+    return value;
+  }
+
+  function renderSection(name, stack, callback, inverted) {
+    var buffer = "";
+    var value =  lookup(name, stack);
+
+    if (inverted) {
+      // From the spec: inverted sections may render text once based on the
+      // inverse value of the key. That is, they will be rendered if the key
+      // doesn't exist, is false, or is an empty list.
+      if (value == null || value === false || (isArray(value) && value.length === 0)) {
+        buffer += callback();
+      }
+    } else if (isArray(value)) {
+      forEach(value, function (value) {
+        stack.push(value);
+        buffer += callback();
+        stack.pop();
+      });
+    } else if (typeof value === "object") {
+      stack.push(value);
+      buffer += callback();
+      stack.pop();
+    } else if (typeof value === "function") {
+      var scope = stack[stack.length - 1];
+      var scopedRender = function (template) {
+        return render(template, scope);
+      };
+      buffer += value.call(scope, callback(), scopedRender) || "";
+    } else if (value) {
+      buffer += callback();
+    }
+
+    return buffer;
+  }
+
+  /**
+   * Parses the given `template` and returns the source of a function that,
+   * with the proper arguments, will render the template. Recognized options
+   * include the following:
+   *
+   *   - file     The name of the file the template comes from (displayed in
+   *              error messages)
+   *   - tags     An array of open and close tags the `template` uses. Defaults
+   *              to the value of Mustache.tags
+   *   - debug    Set `true` to log the body of the generated function to the
+   *              console
+   *   - space    Set `true` to preserve whitespace from lines that otherwise
+   *              contain only a {{tag}}. Defaults to `false`
+   */
+  function parse(template, options) {
+    options = options || {};
+
+    var tags = options.tags || exports.tags,
+        openTag = tags[0],
+        closeTag = tags[tags.length - 1];
+
+    var code = [
+      'var buffer = "";', // output buffer
+      "\nvar line = 1;", // keep track of source line number
+      "\ntry {",
+      '\nbuffer += "'
+    ];
+
+    var spaces = [],      // indices of whitespace in code on the current line
+        hasTag = false,   // is there a {{tag}} on the current line?
+        nonSpace = false; // is there a non-space char on the current line?
+
+    // Strips all space characters from the code array for the current line
+    // if there was a {{tag}} on it and otherwise only spaces.
+    var stripSpace = function () {
+      if (hasTag && !nonSpace && !options.space) {
+        while (spaces.length) {
+          code.splice(spaces.pop(), 1);
+        }
+      } else {
+        spaces = [];
+      }
+
+      hasTag = false;
+      nonSpace = false;
+    };
+
+    var sectionStack = [], updateLine, nextOpenTag, nextCloseTag;
+
+    var setTags = function (source) {
+      tags = trim(source).split(/\s+/);
+      nextOpenTag = tags[0];
+      nextCloseTag = tags[tags.length - 1];
+    };
+
+    var includePartial = function (source) {
+      code.push(
+        '";',
+        updateLine,
+        '\nvar partial = partials["' + trim(source) + '"];',
+        '\nif (partial) {',
+        '\n  buffer += render(partial,stack[stack.length - 1],partials);',
+        '\n}',
+        '\nbuffer += "'
+      );
+    };
+
+    var openSection = function (source, inverted) {
+      var name = trim(source);
+
+      if (name === "") {
+        throw debug(new Error("Section name may not be empty"), template, line, options.file);
+      }
+
+      sectionStack.push({name: name, inverted: inverted});
+
+      code.push(
+        '";',
+        updateLine,
+        '\nvar name = "' + name + '";',
+        '\nvar callback = (function () {',
+        '\n  return function () {',
+        '\n    var buffer = "";',
+        '\nbuffer += "'
+      );
+    };
+
+    var openInvertedSection = function (source) {
+      openSection(source, true);
+    };
+
+    var closeSection = function (source) {
+      var name = trim(source);
+      var openName = sectionStack.length != 0 && sectionStack[sectionStack.length - 1].name;
+
+      if (!openName || name != openName) {
+        throw debug(new Error('Section named "' + name + '" was never opened'), template, line, options.file);
+      }
+
+      var section = sectionStack.pop();
+
+      code.push(
+        '";',
+        '\n    return buffer;',
+        '\n  };',
+        '\n})();'
+      );
+
+      if (section.inverted) {
+        code.push("\nbuffer += renderSection(name,stack,callback,true);");
+      } else {
+        code.push("\nbuffer += renderSection(name,stack,callback);");
+      }
+
+      code.push('\nbuffer += "');
+    };
+
+    var sendPlain = function (source) {
+      code.push(
+        '";',
+        updateLine,
+        '\nbuffer += lookup("' + trim(source) + '",stack,"");',
+        '\nbuffer += "'
+      );
+    };
+
+    var sendEscaped = function (source) {
+      code.push(
+        '";',
+        updateLine,
+        '\nbuffer += escapeHTML(lookup("' + trim(source) + '",stack,""));',
+        '\nbuffer += "'
+      );
+    };
+
+    var line = 1, c, callback;
+    for (var i = 0, len = template.length; i < len; ++i) {
+      if (template.slice(i, i + openTag.length) === openTag) {
+        i += openTag.length;
+        c = template.substr(i, 1);
+        updateLine = '\nline = ' + line + ';';
+        nextOpenTag = openTag;
+        nextCloseTag = closeTag;
+        hasTag = true;
+
+        switch (c) {
+        case "!": // comment
+          i++;
+          callback = null;
+          break;
+        case "=": // change open/close tags, e.g. {{=<% %>=}}
+          i++;
+          closeTag = "=" + closeTag;
+          callback = setTags;
+          break;
+        case ">": // include partial
+          i++;
+          callback = includePartial;
+          break;
+        case "#": // start section
+          i++;
+          callback = openSection;
+          break;
+        case "^": // start inverted section
+          i++;
+          callback = openInvertedSection;
+          break;
+        case "/": // end section
+          i++;
+          callback = closeSection;
+          break;
+        case "{": // plain variable
+          closeTag = "}" + closeTag;
+          // fall through
+        case "&": // plain variable
+          i++;
+          nonSpace = true;
+          callback = sendPlain;
+          break;
+        default: // escaped variable
+          nonSpace = true;
+          callback = sendEscaped;
+        }
+
+        var end = template.indexOf(closeTag, i);
+
+        if (end === -1) {
+          throw debug(new Error('Tag "' + openTag + '" was not closed properly'), template, line, options.file);
+        }
+
+        var source = template.substring(i, end);
+
+        if (callback) {
+          callback(source);
+        }
+
+        // Maintain line count for \n in source.
+        var n = 0;
+        while (~(n = source.indexOf("\n", n))) {
+          line++;
+          n++;
+        }
+
+        i = end + closeTag.length - 1;
+        openTag = nextOpenTag;
+        closeTag = nextCloseTag;
+      } else {
+        c = template.substr(i, 1);
+
+        switch (c) {
+        case '"':
+        case "\\":
+          nonSpace = true;
+          code.push("\\" + c);
+          break;
+        case "\r":
+          // Ignore carriage returns.
+          break;
+        case "\n":
+          spaces.push(code.length);
+          code.push("\\n");
+          stripSpace(); // Check for whitespace on the current line.
+          line++;
+          break;
+        default:
+          if (isWhitespace(c)) {
+            spaces.push(code.length);
+          } else {
+            nonSpace = true;
+          }
+
+          code.push(c);
+        }
+      }
+    }
+
+    if (sectionStack.length != 0) {
+      throw debug(new Error('Section "' + sectionStack[sectionStack.length - 1].name + '" was not closed properly'), template, line, options.file);
+    }
+
+    // Clean up any whitespace from a closing {{tag}} that was at the end
+    // of the template without a trailing \n.
+    stripSpace();
+
+    code.push(
+      '";',
+      "\nreturn buffer;",
+      "\n} catch (e) { throw {error: e, line: line}; }"
+    );
+
+    // Ignore `buffer += "";` statements.
+    var body = code.join("").replace(/buffer \+= "";\n/g, "");
+
+    if (options.debug) {
+      if (typeof console != "undefined" && console.log) {
+        console.log(body);
+      } else if (typeof print === "function") {
+        print(body);
+      }
+    }
+
+    return body;
+  }
+
+  /**
+   * Used by `compile` to generate a reusable function for the given `template`.
+   */
+  function _compile(template, options) {
+    var args = "view,partials,stack,lookup,escapeHTML,renderSection,render";
+    var body = parse(template, options);
+    var fn = new Function(args, body);
+
+    // This anonymous function wraps the generated function so we can do
+    // argument coercion, setup some variables, and handle any errors
+    // encountered while executing it.
+    return function (view, partials) {
+      partials = partials || {};
+
+      var stack = [view]; // context stack
+
+      try {
+        return fn(view, partials, stack, lookup, escapeHTML, renderSection, render);
+      } catch (e) {
+        throw debug(e.error, template, e.line, options.file);
+      }
+    };
+  }
+
+  // Cache of pre-compiled templates.
+  var _cache = {};
+
+  /**
+   * Clear the cache of compiled templates.
+   */
+  function clearCache() {
+    _cache = {};
+  }
+
+  /**
+   * Compiles the given `template` into a reusable function using the given
+   * `options`. In addition to the options accepted by Mustache.parse,
+   * recognized options include the following:
+   *
+   *   - cache    Set `false` to bypass any pre-compiled version of the given
+   *              template. Otherwise, a given `template` string will be cached
+   *              the first time it is parsed
+   */
+  function compile(template, options) {
+    options = options || {};
+
+    // Use a pre-compiled version from the cache if we have one.
+    if (options.cache !== false) {
+      if (!_cache[template]) {
+        _cache[template] = _compile(template, options);
+      }
+
+      return _cache[template];
+    }
+
+    return _compile(template, options);
+  }
+
+  /**
+   * High-level function that renders the given `template` using the given
+   * `view` and `partials`. If you need to use any of the template options (see
+   * `compile` above), you must compile in a separate step, and then call that
+   * compiled function.
+   */
+  function render(template, view, partials) {
+    return compile(template)(view, partials);
+  }
+
+})(Mustache);
diff --git a/sweettooth/static/js/paginator.js b/sweettooth/static/js/paginator.js
index af3fdbf..881b21f 100644
--- a/sweettooth/static/js/paginator.js
+++ b/sweettooth/static/js/paginator.js
@@ -1,7 +1,7 @@
 "use strict";
 
-define(['jquery', 'hashparamutils',
-        'dbus!_', 'jquery.hashchange'], function($, hashparamutils, dbusProxy) {
+define(['jquery', 'hashparamutils', 'dbus!_', 'templates',
+        'jquery.hashchange'], function($, hashparamutils, dbusProxy, templates) {
 
     $.fn.paginatorify = function(url, additionalHashParams, context) {
         if (!this.length)
@@ -12,8 +12,7 @@ define(['jquery', 'hashparamutils',
         if (context === undefined)
             context = 3;
 
-        var $loadingPageContent = $('<div>', {'class': 'loading-page'}).
-            text("Loading page... please wait");
+        var $loadingPageContent = $(templates.paginator.loading_page());
 
         var $elem = $(this);
         var numPages = 0;
diff --git a/sweettooth/static/js/templates.js b/sweettooth/static/js/templates.js
new file mode 100644
index 0000000..2081ace
--- /dev/null
+++ b/sweettooth/static/js/templates.js
@@ -0,0 +1,25 @@
+"use strict";
+
+define(['templates/templatedata', 'mustache'], function(templatedata) {
+    var module = {};
+    module._T = templatedata;
+
+    function compile(template) {
+        // We have our own template caching, don't use Mustache's.
+        return Mustache.compile(v, { cache: false });
+    }
+
+    function _compileTemplateData(data, out) {
+        for (var propname in data) {
+            var v = data[propname];
+            if (typeof(v) === typeof({}))
+                out[propname] = _compileTemplateData(v, {});
+            else
+                out[propname] = compile(v);
+        }
+        return out;
+    }
+
+    _compileTemplateData(templatedata, module);
+    return module;
+});
diff --git a/sweettooth/static/js/templates/build_templates.py b/sweettooth/static/js/templates/build_templates.py
new file mode 100644
index 0000000..ec04a84
--- /dev/null
+++ b/sweettooth/static/js/templates/build_templates.py
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+
+import json
+import os
+import os.path
+
+compile_template = "c(%s)"
+
+def _build_templates(directory):
+    templates = {}
+    for filename in os.listdir(directory):
+        joined = os.path.join(directory, filename)
+        name, ext = os.path.splitext(filename)
+        if os.path.isdir(joined):
+            templates[name] = _build_templates(joined)
+        elif ext == ".mustache":
+            f = open(joined, 'r')
+            templates[name] = f.read().strip()
+            f.close()
+    return templates
+
+def build_templates(directory):
+    templates = _build_templates(directory)
+    f = open(os.path.join(directory, 'templatedata.js'), 'w')
+    f.write("""
+"use strict";
+
+define(%s);
+""" % (json.dumps(templates),))
+    f.close()
+
+if __name__ == "__main__":
+    templates_dir = os.path.realpath(os.path.dirname(__file__))
+    build_templates(templates_dir)
diff --git a/sweettooth/static/js/templates/paginator/loading_page.mustache b/sweettooth/static/js/templates/paginator/loading_page.mustache
new file mode 100644
index 0000000..fc531c7
--- /dev/null
+++ b/sweettooth/static/js/templates/paginator/loading_page.mustache
@@ -0,0 +1,3 @@
+<div class="loading-page">
+  Loading page... please wait.
+</div>
diff --git a/sweettooth/static/js/templates/templatedata.js b/sweettooth/static/js/templates/templatedata.js
new file mode 100644
index 0000000..792e88d
--- /dev/null
+++ b/sweettooth/static/js/templates/templatedata.js
@@ -0,0 +1,4 @@
+
+"use strict";
+
+define({"paginator": {"loading_page": "<div class=\"loading-page\">\n  Loading page... please wait.\n</div>"}, "upgrade": {"need_upgrade": "You have version {{current_version}} of \"{{extension_name}}\". The latest version is version {{latest_version}}. Click here to upgrade.", "latest_version": "You have the latest version of {{extension_name}}."}});
diff --git a/sweettooth/static/js/templates/upgrade/latest_version.mustache b/sweettooth/static/js/templates/upgrade/latest_version.mustache
new file mode 100644
index 0000000..2c1c07d
--- /dev/null
+++ b/sweettooth/static/js/templates/upgrade/latest_version.mustache
@@ -0,0 +1 @@
+You have the latest version of {{extension_name}}.
diff --git a/sweettooth/static/js/templates/upgrade/need_upgrade.mustache b/sweettooth/static/js/templates/upgrade/need_upgrade.mustache
new file mode 100644
index 0000000..6597b9f
--- /dev/null
+++ b/sweettooth/static/js/templates/upgrade/need_upgrade.mustache
@@ -0,0 +1 @@
+You have version {{current_version}} of "{{extension_name}}". The latest version is version {{latest_version}}. Click here to upgrade.



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]