/*! ** jQuery Markup -- jQuery Template Based Markup Generation ** Copyright (c) 2013-2014 Ralf S. Engelschall ** ** 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. */ /* global jQuery: true */ (function ($) { /* jshint -W014 */ /* the storage of all markup expansion functions */ var markup = {}; /* local context-sensitive markup-expansion API function */ $.fn.extend({ markup: function (id, data) { var result = this; this.each(function () { var el = $.markup(id, data); $(this).append(el); result = el; }); return result; } }); /* global context-free markup-expansion API function */ $.markup = function (id, data) { if (typeof markup[id] === "undefined") throw new Error("jquery: markup: ERROR: no such markup with id '" + id + "' found"); return $($.parseHTML($.markup.render(id, data))); }; /* plugin version number */ $.markup.version = "1.0.31"; /* debug level */ $.markup.debug = 0; var debug = function (level, msg) { /* global console: true */ if ( $.markup.debug >= level && typeof console !== "undefined" && typeof console.log === "function") console.log("jquery: markup: DEBUG[" + level + "]: " + msg); }; var esctxt = function (txt) { return "\"" + txt.replace(/\r/g, "\\r") .replace(/\n/g, "\\n") .replace(/"/g, "\\\"") + "\""; }; /* template language registry */ var registry = {}; $.markup.register = function (lang) { registry[lang.id] = lang; }; /* compile a markup into an expansion function */ $.markup.compile = function (type, id, txt) { debug(1, "compile: type=" + type + " id=" + id); debug(4, "compile: txt=" + esctxt(txt)); /* sanity check usage */ if (typeof registry[type] === "undefined") throw new Error("jquery: markup: ERROR: no template language registered under id '" + type + "'"); if (!registry[type].available()) throw new Error("jquery: markup: ERROR: template language '" + type + "' (" + registry[type].name + ", " + registry[type].url + ") " + "known but not found under run-time"); /* remove all leading and trailing whitespaces (as usually one wants a single DOM element back without any surrounding text elements!) */ txt = txt.replace(/^\s+/, "").replace(/\s+$/, ""); /* compile with registered template engine */ markup[id] = registry[type].compile(txt); }; /* parse a markup definition document */ $.markup.parse = function (txt, type) { if (typeof type === "undefined") type = "plain"; debug(3, "parse: type=" + type + " txt=" + esctxt(txt)); var section = []; section.push({ txt: "", attr: { id: "", type: type, include: "false" }}); var m; while (txt !== "") { /* section opening tag */ if ((m = txt.match(/^/)) !== null) { /* parse key="value" attributes */ debug(4, "parse: section opening tag: " + esctxt(m[0])); var attr = {}; var txt2 = m[1], m2; while ((m2 = txt2.match(/^\s+([a-z]+)="([^"]*)"/)) !== null) { debug(4, "parse: section attribute: name=" + esctxt(m2[1]) + " value=" + esctxt(m2[2])); attr[m2[1]] = m2[2].match(/^true|false$/) ? (m2[2] === "true") : m2[2]; txt2 = txt2.substr(m2[0].length); } if (typeof attr.id === "undefined") throw new Error("jquery: markup: ERROR: missing required 'id' attribute"); if (attr.id.match(/^[^\/]+$/) === null) throw new Error("jquery: markup: ERROR: invalid 'id' attribute"); if (section[section.length - 1].attr.id !== "") attr.id = section[section.length - 1].attr.id + "/" + attr.id; if (typeof attr.type === "undefined") attr.type = section[section.length - 1].attr.type; if (typeof attr.include === "undefined") attr.include = false; if (typeof attr.wrap === "undefined") attr.wrap = false; if (typeof attr.trim === "undefined") attr.trim = false; /* open new section */ section.push({ txt: "", attr: attr }); txt = txt.substr(m[0].length); } /* section closing tag */ else if ((m = txt.match(/^<\/markup\s*>/)) !== null) { /* close current section */ debug(4, "parse: section closing tag: " + esctxt(m[0])); var s = section.pop(); if (s.attr.wrap) { /* optionally wrap markup with a root element and a CSS class which is derived from the section id */ var clazz = s.attr.id.replace(/\//g, "-").replace(/[^a-zA-Z0-9_-]/g, "_"); s.txt = "
" + s.txt + "
"; debug(4, "post-process: wrapped markup: " + esctxt(s.txt)); } if (s.attr.trim) { /* optionally trim all whitespaces around tags */ var src = s.txt, dst = "", m3, i, matched; var spec = [ /^\s*()\s*/, /^\s*(<[a-zA-Z_][a-zA-Z0-9_:-]*(?:\s+[a-z]+="[^"]*")*\s*\/?\s*>)\s*/, /^\s*(<\/[a-zA-Z_][a-zA-Z0-9_:-]*\s*>)\s*/, /^([^\s<]+)/, /^(\s+)/ ]; while (src !== "") { matched = false; for (i = 0; i < spec.length; i++) { if ((m3 = src.match(spec[i])) !== null) { dst += m3[1]; src = src.substr(m3[0].length); matched = true; break; } } if (!matched) throw new Error("jquery: markup: ERROR: during markup trimming: " + "unrecognized next token at " + esctxt(src)); } s.txt = dst; debug(4, "post-process: trimmed markup: " + esctxt(s.txt)); } $.markup.compile(s.attr.type, s.attr.id, s.txt); if (s.attr.include) section[section.length - 1].txt += s.txt; txt = txt.substr(m[0].length); } /* plain-text between tags (arbitray long) */ else if ((m = txt.match(/^((?:.|\r?\n)+?)(?=<\/?markup|$)/)) !== null) { /* append plain-text to current section */ debug(4, "parse: plain-text (arbitrary long): " + esctxt(m[0])); section[section.length - 1].txt += m[1]; txt = txt.substr(m[0].length); } /* plain-text between tags (single character fallback) */ else { /* append plain-text to current section */ debug(4, "parse: plain-text (single character fallback): " + esctxt(txt.substr(0, 1))); section[section.length - 1].txt += txt.substr(0, 1); txt = txt.substr(1); } } }; /* queue markup loading requests */ var queue = []; $.markup.queue = function (url, type) { debug(2, "queue: url=" + url + " type=" + type); queue.push({ url: url, type: type }); }; /* asynchronously load all queued markup */ $.markup.load = function (onDone) { debug(1, "load: loading all queued markup requests"); var todo = queue; queue = []; var done = 0; for (var i = 0; i < todo.length; i++) { var type = todo[i].type; debug(2, "load: url=" + todo[i].url + " type=" + type); $.get(todo[i].url, function (txt) { $.markup.parse(txt, type); done += 1; if (done >= todo.length) onDone(); }); } }; /* render a compiled markup and return the textual result */ $.markup.render = function (id, data) { if (typeof markup[id] === "undefined") throw new Error("jquery: markup: ERROR: no such markup with id '" + id + "' found"); return markup[id](data); }; /* automatically process all tags */ /* global document: true */ $(document).ready(function () { debug(1, "ready: processing all tags"); $("head > link").each(function () { var h = $(this).attr("href"); var r = $(this).attr("rel"); var t = $(this).attr("type"); if (typeof h !== "undefined" && h !== "" && typeof r !== "undefined" && r !== "" && typeof t !== "undefined" && t !== "" ) { var mr = r.match(/^markup(?:\/([a-z]+))?$/); var mt = t.match(/^text\/(?:html|x-markup-([a-z]+))$/); if (mr !== null && mt !== null) { var type = (typeof mr[1] === "string" && mr[1] !== "") ? mr[1] : (typeof mt[1] === "string" && mt[1] !== "") ? mt[1] : "plain"; $.markup.queue(h, type); } } }); }); /* helper function for checking that a function is available */ var isfn = function (path) { /* global window: true */ var p = path.split(/\./); var o = window; for (var i = 0; i < p.length; i++) { o = o[p[i]]; if ( (i < p.length - 1 && typeof o === "undefined") || (i === p.length - 1 && typeof o !== "function" )) return false; } return true; }; /* helper function for simple templating (structured value derefering only) */ var simple = function (txt) { var func = ""; var outside = true; var expr, m; while (txt !== "") { if (outside && txt.substr(0, 1) === "\"") { func += "\\\""; txt = txt.substr(1); } else if (outside && (m = txt.match(/^[\r\n]/)) !== null) { func += "\\" + (m[0] === "\r" ? "r" : "n"); txt = txt.substr(m[0].length); } else if (outside && txt.substr(0, 2) === "{{") { outside = false; expr = "data"; txt = txt.substr(2); } else if (!outside && (m = txt.match(/^\s+/)) !== null) { txt = txt.substr(m[0].length); } else if (!outside && (m = txt.match(/^(?:\.)?([a-zA-Z_][a-zA-Z0-9_]*)/)) !== null) { expr = "deref(" + expr + ", \"" + m[1] + "\")"; txt = txt.substr(m[0].length); } else if (!outside && (m = txt.match(/^\[(\d+)\]/)) !== null) { expr = "deref(" + expr + ", " + m[1] + ")"; txt = txt.substr(m[0].length); } else if (!outside && (m = txt.match(/^\["((?:\\"|.)*)"\]/)) !== null) { expr = "deref(" + expr + ", \"" + m[1] + "\")"; txt = txt.substr(m[0].length); } else if (!outside && (m = txt.match(/^\['((?:\\'|.)*)'\]/)) !== null) { expr = "deref(" + expr + ", '" + m[1] + "')"; txt = txt.substr(m[0].length); } else if (!outside && txt.substr(0, 2) === "}}") { func += "\" + " + expr + " + \""; outside = true; txt = txt.substr(2); } else if (outside && (m = txt.match(/^(.+?)(?=\r|\n|"|\{\{|$)/)) !== null) { func += m[1]; txt = txt.substr(m[0].length); } else { func += txt.substr(0, 1); txt = txt.substr(1); } } /* jshint -W054 */ return new Function("data", "var deref = function (obj, sel) {" + "return (typeof obj === \"object\" ? obj[sel] : undefined);" + "};" + "return \"" + func + "\";" ); }; /* helper function for registering a template engine */ var reg = function (id, name, url, func, comp) { $.markup.register({ id: id, name: name, url: url, available: (typeof func === "function" ? func : function () { return isfn(func); }), compile: comp }); }; /* Plain HTML (efficient: pass-through only, incomplete: no data) */ reg("plain", "Plain HTML", "-", function () { return true; }, function (txt) { return function (/* data */) { return txt; }; }); /* Simple HTML (efficient: pre-compilation, complete: data support) */ reg("simple", "Simple HTML", "-", function () { return true; }, function (txt) { return simple(txt); }); /* Handlebars (efficient: pre-compilation, complete: data support) */ reg("handlebars", "Handlebars", "http://handlebarsjs.com/", "Handlebars.compile", function (txt) { /* global Handlebars: true */ return Handlebars.compile(txt); }); /* Emblem (indented Handlebars) (efficient: pre-compilation, complete: data support) */ reg("emblem", "Emblem", "http://emblemjs.com/", function () { return isfn("Handlebars") && isfn("Emblem.compile"); }, function (txt) { /* global Emblem: true */ return Emblem.compile(Handlebars, txt); }); /* DUST (efficient: pre-compilation, complete: data support) */ reg("dust", "DUST", "http://linkedin.github.io/dustjs/", "dust.compile", function (txt) { /* global dust: true */ return dust.compile(txt); }); /* Jade (efficient: pre-compilation, complete: data support) */ reg("jade", "Jade", "http://jade-lang.com/", "jade.compile", function (txt) { /* global jade: true */ return jade.compile(txt); }); /* Mustache (efficient: pre-compilation, complete: data support) */ reg("mustache", "Mustache", "http://mustache.github.io/", "Mustache.compile", function (txt) { /* global Mustache: true */ return Mustache.compile(txt); }); /* Walrus (efficient: pre-compilation, complete: data support) */ reg("walrus", "Walrus", "http://documentup.com/jeremyruppel/walrus/", "Walrus.Parser.parse", function (txt) { /* global Walrus: true */ var tmpl = Walrus.Parser.parse(txt); return function (data) { return tmpl.compile(data); }; }); /* HAML-JS (efficient: pre-compilation, complete: data support) */ reg("haml", "HAML-JS", "https://github.com/creationix/haml-js", "Haml", function (txt) { /* global Haml: true */ return Haml(txt); }); /* doT (efficient: pre-compilation, complete: data support) */ reg("dot", "doT", "http://olado.github.io/doT/", "doT.template", function (txt) { /* global doT: true */ return doT.template(txt); }); /* rssi (efficient: pre-compilation, complete: data support) */ reg("rssi", "rssi", "https://github.com/mvasilkov/rssi", "fmt", function (txt) { /* global fmt: true */ return fmt(txt); }); /* Hogan (efficient: pre-compilation, complete: data support) */ reg("hogan", "Twitter Hogan", "http://twitter.github.io/hogan.js/", "hogan.compile", function (txt) { /* global hogan: true */ var tmpl = hogan.compile(txt); return function (data) { return tmpl.render(data); }; }); /* Underscore Template (efficient: pre-compilation, complete: data support) */ reg("underscore", "Underscore Template", "http://underscorejs.org/", "_.template", function (txt) { /* global _: true */ return _.template(txt); }); /* Jiko (efficient: on-the-fly compilation, complete: data support) */ reg("jiko", "Jiko", "http://jiko.neoname.eu/", "jiko.loadTemplate", function (txt) { /* global jiko: true */ return jiko.loadTemplate(txt); }); /* Qatrix Template (efficient: cached on-the-fly compilation, complete: data support) */ reg("qatrix", "Qatrix Template", "http://qatrix.com/", "Qatrix.$template", function (txt) { return function (data) { /* global Qatrix: true */ return Qatrix.template(txt, data); }; }); /* Teddy (efficient: pre-compilation, complete: data support) */ reg("teddy", "Teddy", "https://github.com/kethinov/teddy", "teddy.compile", function (txt) { /* global teddy: true */ var name = txt /* hack */; teddy.compile(txt, name); return function (data) { return teddy.render(teddy.compiledTemplates[name], data); }; }); /* Swig (efficient: pre-compilation, complete: data support) */ reg("swig", "Swig", "http://paularmstrong.github.io/swig/", "swig.compile", function (txt) { /* global swig: true */ return swig.compile(txt); }); /* EJS (efficient: pre-compilation, complete: data support) */ reg("ejs", "EJS", "http://embeddedjs.com/", "EJS", function (txt) { /* global EJS: true */ var tmpl = EJS({ text: txt }); return function (data) { return tmpl.render(data); }; }); /* JST (efficient: pre-compilation, complete: data support) */ reg("jst", "JST", "http://code.google.com/p/trimpath/wiki/JavaScriptTemplates", "TrimPath.parseTemplate", function (txt) { /* global TrimPath: true */ var tmpl = TrimPath.parseTemplate(txt); return function (data) { return tmpl.process(data); }; }); /* Nunjucks (efficient: on-the-fly compilation, complete: data support) */ reg("nunjucks", "Nunjucks", "http://nunjucks.jlongster.com/", "nunjucks.Template", function (txt) { /* global nunjucks: true */ var tmpl = new nunjucks.Template(txt); return function (data) { return tmpl.render(data); }; }); /* JsRender (efficient: on-the-fly compilation, complete: data support) */ reg("jsrender", "JsRender", "http://www.jsviews.com/#jsrender", "$.templates", function (txt) { var tmpl = $.templates(txt); return function (data) { return tmpl.render(data); }; }); /* Combyne (efficient: on-the-fly compilation, complete: data support) */ reg("combyne", "Combyne", "https://github.com/tbranyen/combyne", "combyne", function (txt) { /* global combyne: true */ var tmpl = combyne(txt); return function (data) { return tmpl.render(data); }; }); /* Markup.js (inefficient: on-the-fly compilation, complete: data support) */ reg("markup", "Markup.js", "https://github.com/adammark/Markup.js/", "Mark.up", function (txt) { return function (data) { /* global Mark: true */ return Mark.up(txt, data); }; }); /* Plates (inefficient: on-the-fly compilation, complete: data support) */ reg("plates", "Plates", "https://github.com/flatiron/plates", "Plates.bind", function (txt) { return function (data) { /* global Plates: true */ return Plates.bind(txt, data); }; }); /* Emmet markup (inefficient: on-the-fly compilation, incomplete: no data support) */ reg("markup", "Markup.js", "http://emmet.io/", "emmet.expandAbbreviation", function (txt) { return function (/* data */) { /* global emmet: true */ return emmet.expandAbbreviation(txt); }; }); })(jQuery);