[extensions-web] js: Import mustache, introduce a new templates system
- From: Jasper St. Pierre <jstpierre src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [extensions-web] js: Import mustache, introduce a new templates system
- Date: Sat, 31 Mar 2012 20:48:04 +0000 (UTC)
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 = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': '"',
+ "'": '''
+ };
+
+ 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]