/* Copyright (c) 2013 Ed Spencer 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. */ /** * @class Jaml * @author Ed Spencer (http://edspencer.net) * Jaml is a simple JavaScript library which makes HTML generation easy and pleasurable. * Examples: http://edspencer.github.com/jaml * Introduction: http://edspencer.net/2009/11/jaml-beautiful-html-generation-for-javascript.html */ Jaml = function() { return { templates: {}, /** * Registers a template by name * @param {String} name The name of the template * @param {Function} template The template function */ register: function(name, template) { this.templates[name] = template; }, /** * Renders the given template name with an optional data object * @param {String} name The name of the template to render * @param {Object} thisObj Optional data object * @param {Object} data Optional data object */ render: function(name, thisObj, data) { var template = this.templates[name], renderer = new Jaml.Template(template); return renderer.render.apply(renderer, Array.prototype.slice.call(arguments, 1)); } }; }(); /** * @constructor * @param {String} tagName The tag name this node represents (e.g. 'p', 'div', etc) */ Jaml.Node = function(tagName) { /** * @property tagName * @type String * This node's current tag */ this.tagName = tagName; /** * @property attributes * @type Object * Sets of attributes on this node (e.g. 'cls', 'id', etc) */ this.attributes = {}; /** * @property children * @type Array * Array of rendered child nodes that will be included as this node's innerHTML */ this.children = []; }; Jaml.Node.prototype = { /** * Adds attributes to this node * @param {Object} attrs Object containing key: value pairs of node attributes */ setAttributes: function(attrs) { for (var key in attrs) { //convert cls to class var mappedKey = key == 'cls' ? 'class' : key; this.attributes[mappedKey] = attrs[key]; } return this; }, /** * Adds a child string to this node. This can be called as often as needed to add children to a node * @param {String} childText The text of the child node */ addChild: function(childText) { this.children.push(childText); return this; }, /** * Renders this node with its attributes and children * @param {Number} lpad Amount of whitespace to add to the left of the string (defaults to 0) * @return {String} The rendered node */ render: function(lpad) { lpad = lpad || 0; var node = [], attrs = [], textnode = (this instanceof Jaml.TextNode), multiline = this.multiLineTag(); //add any left padding if (!textnode) node.push(this.getPadding(lpad)); //open the tag node.push("<" + this.tagName); for (var key in this.attributes) { attrs.push(key + "=\"" + this.attributes[key] + "\""); } attrs.sort() //add any tag attributes for (var i=0; i\n"); } else { node.push(">"); if (multiline) node.push("\n"); this.renderChildren(node, this.children, lpad); if (multiline) node.push(this.getPadding(lpad)); node.push("\n"); } return node.join(""); }, /** * Renders an array of children * @node {Array} the current array of rendered lines * @children {Array} the child nodes to be rendered * @param {Number} lpad Amount of whitespace to add to the left of the string */ renderChildren: function(node, children, lpad) { var childLpad = lpad + 2; for (var i=0; i < children.length; i++) { var child = children[i]; if (child instanceof Array) { var nestedChildren = child; this.renderChildren(node, nestedChildren, lpad) } else { node.push(child.render(childLpad)); } } }, /** * Returns true if this tag should be rendered with multiple newlines (e.g. if it contains child nodes) * @return {Boolean} True to render this tag as multi-line */ multiLineTag: function() { var childLength = this.children.length, multiLine = childLength > 0; if (childLength == 1 && this.children[0] instanceof Jaml.TextNode) multiLine = false; return multiLine; }, /** * Returns a string with the given number of whitespace characters, suitable for padding * @param {Number} amount The number of whitespace characters to add * @return {String} A padding string */ getPadding: function(amount) { return new Array(amount + 1).join(" "); }, /** * Returns true if this tag should close itself (e.g. no element) * @return {Boolean} True if this tag should close itself */ isSelfClosing: function() { for (var i = this.notSelfClosingTags.length - 1; i >= 0; i--) { if (this.tagName == this.notSelfClosingTags[i]) return false; } return true; }, /** * @property selfClosingTags * @type Array * An array of all tags that should be self closing */ notSelfClosingTags: ['textarea', 'script', 'em', 'strong', 'option', 'select'] }; Jaml.TextNode = function(text) { this.text = text; }; Jaml.TextNode.prototype = { render: function() { return this.text; } }; /** * Represents a single registered template. Templates consist of an arbitrary number * of trees (e.g. there may be more than a single root node), and are not compiled. * When a template is rendered its node structure is computed with any provided template * data, culminating in one or more root nodes. The root node(s) are then joined together * and returned as a single output string. * * The render process uses two dirty but necessary hacks. First, the template function is * decompiled into a string (but is not modified), so that it can be eval'ed within the scope * of Jaml.Template.prototype. This allows the second hack, which is the use of the 'with' keyword. * This allows us to keep the pretty DSL-like syntax, though is not as efficient as it could be. */ Jaml.Template = function(tpl) { /** * @property tpl * @type Function * The function this template was created from */ this.tpl = tpl; this.nodes = []; }; Jaml.Template.prototype = { /** * Renders this template given the supplied data * @param {Object} thisObj Optional data object * @param {Object} data Optional data object * @return {String} The rendered HTML string */ render: function(thisObj, data) { data = data || (thisObj = thisObj || {}); //the 'data' argument can come in two flavours - array or non-array. Normalise it //here so that it always looks like an array. if (data.constructor.toString().indexOf("Array") == -1) { data = [data]; } with(this) { for (var i=0; i < data.length; i++) { eval("(" + this.tpl.toString() + ").call(thisObj, data[i], i)"); }; } var roots = this.getRoots(), output = ""; for (var i=0; i < roots.length; i++) { output += roots[i].render(); }; return output; }, /** * Returns all top-level (root) nodes in this template tree. * Templates are tree structures, but there is no guarantee that there is a * single root node (e.g. a single DOM element that all other elements nest within) * @return {Array} The array of root nodes */ getRoots: function() { var roots = []; for (var i=0; i < this.nodes.length; i++) { var node = this.nodes[i]; if (node.parent == undefined) roots.push(node); }; return roots; }, tags: [ "html", "head", "body", "script", "meta", "title", "link", "div", "p", "span", "a", "img", "br", "hr", "em", "i", "strong", "b", "table", "tr", "th", "td", "thead", "tbody", "tfoot", "ul", "ol", "li", "dl", "dt", "dd", "h1", "h2", "h3", "h4", "h5", "h6", "h7", "form", "fieldset", "input", "textarea", "label", "select", "option" ] }; /** * Adds a function for each tag onto Template's prototype */ (function() { var tags = Jaml.Template.prototype.tags; /** * This function is created for each tag name and assigned to Template's * prototype below */ var makeTagHelper = function(tagName) { return function(attrs) { var node = new Jaml.Node(tagName); var firstArgIsAttributes = (typeof attrs == 'object') && !(attrs instanceof Jaml.Node) && !(attrs instanceof Jaml.TextNode); if (firstArgIsAttributes) node.setAttributes(attrs); var startIndex = firstArgIsAttributes ? 1 : 0; for (var i=startIndex; i < arguments.length; i++) { var arg = arguments[i]; if (typeof arg == "string" || arg == undefined) { arg = new Jaml.TextNode(arg || ""); } if (arg instanceof Jaml.Node || arg instanceof Jaml.TextNode) { arg.parent = node; } node.addChild(arg); }; this.nodes.push(node); return node; }; }; for (var i = 0, tag; tag = tags[i]; i++) { Jaml.Template.prototype[tag] = makeTagHelper(tag); }; })();