|
// Tempest jQuery Templating Plugin |
|
// ================================ |
|
// |
|
// Copyright (c) 2009 Nick Fitzgerald - http://fitzgeraldnick.com/ |
|
// |
|
// Permission is hereby granted, free of charge, to any person obtaining a copy |
|
// of this software and associated documentation files (the "Software"), to deal |
|
// in the Software without restriction, including without limitation the rights |
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
// copies of the Software, and to permit persons to whom the Software is |
|
// furnished to do so, subject to the following conditions: |
|
// |
|
// The above copyright notice and this permission notice shall be included in |
|
// all copies or substantial portions of the Software. |
|
// |
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
// THE SOFTWARE. |
|
|
|
// JSLint |
|
"use strict"; |
|
|
|
(function ($) { |
|
// PRIVATE VARIABLES |
|
var templateCache = {}, |
|
|
|
// TAG REGULAR EXPRESSIONS |
|
// Overwrite these if you want, but don't blame me when stuff goes wrong. |
|
OPEN_VAR_TAG = /\{\{[\s]*?/g, |
|
CLOSE_VAR_TAG = /[\s]*?\}\}/g, |
|
OPEN_BLOCK_TAG = /\{%[\s]*?/g, |
|
CLOSE_BLOCK_TAG = /[\s]*?%\}/g, |
|
|
|
// Probably, you don't want to mess with these, as they are built from |
|
// the ones above. |
|
VAR_TAG = new RegExp(OPEN_VAR_TAG.source + |
|
"[\\w\\-\\.]+?" + |
|
CLOSE_VAR_TAG.source, "g"), |
|
|
|
BLOCK_TAG = new RegExp(OPEN_BLOCK_TAG.source + |
|
"[\\w]+?(?:[ ]+?[\\w\\-\\.]*?)*?" + |
|
CLOSE_BLOCK_TAG.source, "g"), |
|
|
|
END_BLOCK_TAG = new RegExp(OPEN_BLOCK_TAG.source + |
|
"end[\\w]*?" + |
|
CLOSE_BLOCK_TAG.source, "g"), |
|
|
|
// All block tags stored in here. Tags have a couple things to work |
|
// with: |
|
// |
|
// * "args" property is set before render: |
|
// - Example: {% tag_type arg1 arg2 foo bar %} |
|
// * The "args" property would be set to |
|
// ["arg1", "arg2", "foo", "bar"] |
|
// in this example. The tag's render method could look them |
|
// up in the context object, or could do whatever it wanted |
|
// to do with it. |
|
// * "subNodes" property which is an array of all the nodes between |
|
// the block tag and it's corresponding {% end... %} tag |
|
// - NOTE: This property is only set for a block if it has the |
|
// "expectsEndTag" property set to true. |
|
// * Every block tag should have a "render" method that takes one |
|
// argument: a context object. It should return a string. |
|
BLOCK_NODES = { |
|
"for": { |
|
expectsEndTag: true, |
|
render: function (context) { |
|
var args = this.args, |
|
subNodes = this.subNodes, |
|
renderedNodes = [], |
|
i, itemName, arrName, arr, forContext, tmpObj; |
|
|
|
if (args.length === 3 && args[1] === "in") { |
|
itemName = args[0]; |
|
arrName = args[2]; |
|
arr = getValFromObj(arrName, context); |
|
|
|
for (i = 0; i < arr.length; i++) { |
|
tmpObj = {}; |
|
tmpObj[itemName] = arr[i]; |
|
tmpObj._index = i; |
|
forContext = $.extend(true, {}, context, tmpObj); |
|
|
|
$.each(subNodes, function (j, node) { |
|
renderedNodes.push( |
|
node.render(forContext) |
|
); |
|
}); |
|
} |
|
|
|
return renderedNodes.join(""); |
|
} |
|
else { |
|
throw new TemplateSyntaxError( |
|
"Bad for tag syntax. Use {% for <item> in <array> %}" |
|
); |
|
} |
|
} |
|
}, |
|
"if": { |
|
expectsEndTag: true, |
|
render: function (context) { |
|
var rendered_nodes = [], |
|
subNodes = this.subNodes; |
|
|
|
// Check the truthiness of the argument. |
|
if (!!context[this.args[0]]) { |
|
$.each(subNodes, function (i, node) { |
|
rendered_nodes.push(node.render(context)); |
|
}); |
|
} |
|
|
|
return rendered_nodes.join(""); |
|
} |
|
} |
|
}, |
|
|
|
// Base text node object for prototyping. |
|
baseTextNode = { |
|
render: function (context) { |
|
return this.text || ""; |
|
} |
|
}, |
|
|
|
// Base variable node object for prototyping. |
|
baseVarNode = { |
|
render: function (context) { |
|
var val = context[this.name] === undefined ? |
|
"" : |
|
context[this.name]; |
|
if (val === "" && this.name.search(/\./) !== -1) { |
|
return getValFromObj(this.name, context); |
|
} |
|
return cleanVal(val); |
|
} |
|
}; |
|
|
|
// CUSTOM ERRORS |
|
|
|
function TemplateSyntaxError(message) { |
|
if (!(this instanceof TemplateSyntaxError)) { |
|
return new TemplateSyntaxError(message); |
|
} |
|
this.message = message; |
|
return this; |
|
} |
|
TemplateSyntaxError.prototype = new SyntaxError(); |
|
TemplateSyntaxError.prototype.name = "TemplateSyntaxError"; |
|
|
|
// PRIVATE FUNCTIONS |
|
|
|
// Some browsers don't return the grouped part of the RegExp with the array, |
|
// so we must accomodate them. |
|
var split = (function () { |
|
if ("abc".split(/(b)/).length === 3) { |
|
return function (str, delimiter) { |
|
return String.prototype |
|
.split |
|
.call(str, delimiter); |
|
}; |
|
} else { |
|
return function (str, delimiter) { |
|
if (Object.prototype |
|
.toString |
|
.call(delimiter) === "[object RegExp]") { |
|
var regex = delimiter.ignoreCase ? |
|
new RegExp(delimiter.source, "gi") : |
|
new RegExp(delimiter.source, "g"), |
|
match, |
|
match_str = "", |
|
arr = [], |
|
i, |
|
len = str.length; |
|
|
|
for (i = 0; i < len; i++) { |
|
match_str += str.charAt(i); |
|
match = match_str.match(regex); |
|
if (match !== null && match.length > 0) { |
|
arr.push(match_str.replace(match[0], "")); |
|
arr.push(match[0]); |
|
match_str = ""; |
|
} |
|
} |
|
|
|
if (match_str !== "") { |
|
arr.push(match_str); |
|
} |
|
|
|
return arr; |
|
} else { |
|
return String.prototype |
|
.split |
|
.call(str, delimiter); |
|
} |
|
}; |
|
} |
|
}()); |
|
|
|
function isBlockTag(token) { |
|
return token.search(BLOCK_TAG) !== -1; |
|
} |
|
function isEndTag(token) { |
|
return token.search(END_BLOCK_TAG) !== -1; |
|
} |
|
function isVarTag(token) { |
|
return token.search(VAR_TAG) !== -1; |
|
} |
|
|
|
function strip(str) { |
|
return str.replace(/^[\s]+/, "").replace(/[\s]+$/, ""); |
|
} |
|
|
|
// Clean the passed value the best we can. |
|
function cleanVal(val) { |
|
if (val instanceof $) { |
|
return jQueryToString(val); |
|
} else if (val !== null && !isArray(val) && typeof(val) === "object") { |
|
if (typeof(val.toHTML) === "function") { |
|
return cleanVal(val.toHTML()); |
|
} else { |
|
return val.toString(); |
|
} |
|
} else { |
|
return val; |
|
} |
|
} |
|
|
|
// Traverse a path of an obj from a string representation, |
|
// for example "object.child.attr". |
|
function getValFromObj(str, obj) { |
|
var path = split(str, "."), |
|
val = obj[path[0]], |
|
i; |
|
for (i = 1; i < path.length; i++) { |
|
// Return an empty string if the lookup ever hits undefined. |
|
if (val !== undefined) { |
|
val = val[path[i]]; |
|
} else { |
|
return ""; |
|
} |
|
} |
|
|
|
// Make sure the last piece did not end up undefined. |
|
val = val === undefined ? "" : val; |
|
return cleanVal(val); |
|
} |
|
|
|
// Hack to get the HTML of a jquery object as a string. |
|
function jQueryToString(jq) { |
|
return $(document.createElement("div")).append(jq).html(); |
|
} |
|
|
|
// Make a new copy of a given object. |
|
function makeObj(obj) { |
|
if (obj === undefined) { |
|
return obj; |
|
} |
|
var O = function () {}; |
|
O.prototype = obj; |
|
return new O(); |
|
} |
|
|
|
// Return an array of key/template pairs. |
|
function storedTemplates() { |
|
var cache = []; |
|
$.each(templateCache, function (key, templ) { |
|
cache.push([ key, templ ]); |
|
}); |
|
return cache; |
|
} |
|
|
|
// Determine if the string is a key to a stored template or a |
|
// one-time-use template. |
|
function chooseTemplate(str) { |
|
return typeof templateCache[str] === "string" ? |
|
templateCache[str] : |
|
str; |
|
} |
|
|
|
// Return true if (and only if) an object is an array. |
|
function isArray(objToTest) { |
|
return Object.prototype |
|
.toString |
|
.apply(objToTest) === "[object Array]"; |
|
} |
|
|
|
// Call a rendering function on arrays of objects or just a single |
|
// object seamlessly. |
|
function renderEach(data, f) { |
|
return isArray(data) ? |
|
$.each(data, f) : |
|
f(0, data); |
|
} |
|
|
|
// Split a template in to tokens which will eventually be converted to |
|
// nodes and then rendered. |
|
function tokenize(templ) { |
|
return (function (arr) { |
|
var tokens = []; |
|
for (i = 0; i < arr.length; i++) { |
|
(function (token) { |
|
return token === "" ? |
|
null : |
|
tokens.push(token); |
|
}(arr[i])); |
|
} |
|
return tokens; |
|
}(split(templ, new RegExp("(" + VAR_TAG.source + "|" + |
|
BLOCK_TAG.source + "|" + |
|
END_BLOCK_TAG.source + ")")))); |
|
} |
|
|
|
// "Lisp in C's clothing." - Douglas Crockford |
|
function cdr(arr) { |
|
return arr.slice(1); |
|
} |
|
|
|
// Array.push changes the original array in place and returns the new |
|
// length of the array rather than the the actual array itself. This |
|
// makes it unchainable, which is ridiculous. |
|
function append(item, list) { |
|
return list.concat([item]); |
|
} |
|
|
|
// Take a token and create a variable node from it. |
|
function makeVarNode(token) { |
|
var node = makeObj(baseVarNode); |
|
node.name = strip(token.replace(OPEN_VAR_TAG, "") |
|
.replace(CLOSE_VAR_TAG, "")); |
|
return node; |
|
} |
|
|
|
// Take a token and create a text node from it. |
|
function makeTextNode(token) { |
|
var node = makeObj(baseTextNode); |
|
node.text = token; |
|
return node; |
|
} |
|
|
|
// A recursive function that terminates either when all tokens have |
|
// been converted to nodes or an end-block tag is found. |
|
function makeNodes(tokens) { |
|
return (function (nodes, tokens) { |
|
var token = tokens[0]; |
|
return tokens.length === 0 ? |
|
[nodes, [], true] : |
|
isEndTag(token) ? |
|
[nodes, cdr(tokens)] : |
|
isVarTag(token) ? |
|
arguments.callee(append(makeVarNode(token), nodes), cdr(tokens)) : |
|
isBlockTag(token) ? |
|
makeBlockNode(nodes, tokens, arguments.callee) : |
|
// Else assume it is a text node. |
|
arguments.callee(append(makeTextNode(token), nodes), cdr(tokens)); |