// 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 in %}" ); } } }, "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)); }([], tokens)); } // Split a block tags contents in to an array of bits that contains the // type of block node, and any arguments that were passed to the block // node if they exist. function makeBits(blockToken) { return (function (bits, split) { // Remove empty strings and strip whitespace. for (i = 0; i < split.length; i++) { (function (bit) { return bit === "" ? null : bits.push(bit); }(strip(split[i]))); } return bits; }([], split(blockToken.replace(OPEN_BLOCK_TAG, "") .replace(CLOSE_BLOCK_TAG, ""), /[\s]+?/))); } // Create a block tag's node by hijacking the "makeNodes" function // until an end-block is found. function makeBlockNode(nodes, tokens, f) { // Remove the templating syntax and split the type of block tag and // its arguments. var bits = makeBits(tokens[0]), // The type of block tag is the first of the bits, the rest // (if present) are args type = bits[0], args = cdr(bits), // Make the node from the set of block tags that Tempest knows // about. node = makeObj(BLOCK_NODES[type]), resultsArray; // Ensure that the type of block tag is one that is defined in // BLOCK_NODES if (node === undefined) { throw new TemplateSyntaxError("Unknown Block Tag."); } node.args = args; tokens = cdr(tokens); if (node.expectsEndTag === true) { resultsArray = makeNodes(tokens); if (resultsArray[2] !== undefined) { // The third item in the array returned by makeNodes is // only defined if the last of the tokens was made in to a // node and it wasn't an end-block tag. throw new TemplateSyntaxError( "A block tag was expecting an ending tag but it was not found." ); } node.subNodes = resultsArray[0]; tokens = resultsArray[1]; } // Add the newly created node to the nodes list. nodes = append(node, nodes); // Continue where we were before the block node. return f(nodes, tokens); } // Return the template rendered with the given object(s) as a jQuery // object. function renderToJQ(str, objects) { var template = chooseTemplate(str), lines = []; renderEach(objects, function (i, obj) { var resultsArray = makeNodes(tokenize(template), obj), nodes = resultsArray[0]; // Check for tokens left over in the results array, this means // that not all tokens were rendered because there are more // end-block tagss than block tags that expect an end. if (resultsArray[1].length !== 0) { throw new TemplateSyntaxError( "An unexpected end tag was found." ); } // Render each node and push it to the lines. $.each(nodes, function (i, node) { lines.push(node.render(obj)); }); }); // Return the joined templates as jQuery objects if it appears to start // with an HTML tag, otherwise just return the string itself. return (function (str) { return str.charAt(0) === "<" ? $(str) : str; }(strip(lines.join("")))); } // EXTEND JQUERY OBJECT $.extend({ tempest: function () { var args = arguments; if (args.length === 0) { // Return key/template pairs of all stored templates. return storedTemplates(); } else if (args.length === 2 && typeof(args[0]) === "string" && typeof(args[1]) === "object") { // Render the supplied template (args[0], template name of // existing or one-time-use template) with the context data // (args[1]). return renderToJQ(args[0], args[1]); } else if (args.length === 1 && typeof(args[0]) === "string") { // Template getter. return templateCache[args[0]]; } else if (args.length === 2 && typeof(args[0]) === "string" && typeof(args[1]) === "string") { // Template setter. templateCache[args[0]] = args[1].replace(/^\s+/g, "") .replace(/\s+$/g, "") .replace(/[\n\r]+/g, ""); return templateCache[args[0]]; } else { // Raise an exception because the arguments did not match the // API. throw new TypeError( "jQuery.tempest can't handle the given arguments." ); } } }); // Extend jQuery("selector").tempest using the existing jQuery.tempest API. $.fn.tempest = function() { var args = Array.prototype.slice.call(arguments, 0); var f = null; if (args.length == 2 && typeof args[0] == "string" && typeof args[1] == "object") { // Inserts the result of rendering the specified template on the // specified data into the set of matched elements. f = function () { $(this).html($.tempest(args[0], args[1])); }; } else if (args.length == 3 && typeof args[0] == "string" && typeof args[1] == "string" && typeof args[2] == "object") { // Calls the appropriate jQuery function, passing it the result of // rendering the given template on the data provided. f = function () { $(this)[args[0]]($.tempest(args[1], args[2])); }; } else { throw new TypeError([ "jQuery(selector).tempest was passed the wrong number or type", "of arguments. Received " + args ].join(" ")); } return this.each(f); }; // EXPOSE BLOCK_NODES OBJECT TO ALLOW EXTENSION WITH CUSTOM TAGS $.tempest.tags = BLOCK_NODES; // EXPOSE PRIVATE FUNCTIONS FOR TESTING if (window.testTempestPrivates === true) { $.tempest._test = {}; // Make it easier to attach the private methods methods to the public // object. function a(name, fn) { $.tempest._test[name] = fn; } a("isBlockTag", isBlockTag); a("isEndTag", isEndTag); a("isVarTag", isVarTag); a("cleanVal", cleanVal); a("getValFromObj", getValFromObj); a("jQueryToString", jQueryToString); a("makeObj", makeObj); a("storedTemplates", storedTemplates); a("chooseTemplate", chooseTemplate); a("isArray", isArray); a("renderEach", renderEach); a("tokenize", tokenize); a("cdr", cdr); a("append", append); a("makeVarNode", makeVarNode); a("makeTextNode", makeTextNode); a("makeNodes", makeNodes); a("makeBits", makeBits); a("makeBlockNode", makeBlockNode); a("renderToJQ", renderToJQ); a("strip", strip); } // GET ALL TEXTAREA TEMPLATES ON READY $(document).ready(function () { $(".tempest-template").each(function (obj) { templateCache[$(this).attr('title')] = strip(($(this).val() || $(this).html()).replace(/[\n\r]+/g, " ")); $(this).remove(); }); }); }(jQuery));