diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/luigi-template.js | 379 | ||||
-rw-r--r-- | js/test.js | 58 |
2 files changed, 437 insertions, 0 deletions
diff --git a/js/luigi-template.js b/js/luigi-template.js new file mode 100644 index 0000000..925b811 --- /dev/null +++ b/js/luigi-template.js @@ -0,0 +1,379 @@ +/** + * luigi-template.js + * ================= + * + * Links + * ----- + * * Contact: Paul Duncan (<pabs@pablotron.org>) + * * Home Page: <http://pablotron.org/luigi-template/> + * * Mercurial Repository: <http://hg.pablotron.org/luigi-template/> + * + * Overview + * -------- + * Tiny client-side JavaScript templating library. + * + * Why? This script is: + * + * * less than 4k minified (see `luigi-template.min.js`), + * + * * has no external dependencies (no jQuery/YUI/Sensha), + * + * * works in browsers as old as IE8, and + * + * * MIT licensed (use for whatever, I don't care) + * + * Usage + * ----- + * TODO + * + * License + * ------- + * Copyright (c) 2014 Paul Duncan <pabs@pablotron.org> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * The names of contributors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ +LuigiTemplate = (function() { + "use strict"; + + var VERSION = '0.4.0'; + + // Array.each polyfill + var each = (function() { + if (Array.prototype.forEach) { + return function(a, fn) { + a.forEach(fn); + }; + } else { + return function(a, fn) { + var i, l; + + for (i = 0, l = a.length; i < l; i++) + fn(a[i], i, a); + }; + } + })(); + + // Array.map polyfill + var map = (function() { + if (Array.prototype.map) { + return function(a, fn) { + return a.map(fn); + }; + } else { + return function(a, fn) { + var r = new Array(a.length); + + each(a, function(v, i) { + r[i] = v; + }); + + return r; + }; + } + })(); + + // Array.reduce polyfill + var reduce = (function() { + if (Array.prototype.reduce) { + return function(a, fn, iv) { + return a.reduce(fn, iv); + }; + } else { + return function(a, fn, r) { + each(a, function(v, i) { + r = fn(r, v, i, a); + }); + + return r; + }; + } + })(); + + // String.trim polyfill + var trim = (function() { + if (String.prototype.trim) { + return function(s) { + return (s || '').trim(); + }; + } else { + var re = /^\s+|\s+$/g; + + return function(s) { + (s || '').replace(re, ''); + }; + } + })(); + + // String.scan polyfill + function scan(s, re, fn) { + var m; + + if (!re.global) + throw 'non-global regex'; + + while ((m = re.exec(s)) !== null) + fn(m); + } + + // list of built-in filters + var FILTERS = { + uc: function(v) { + return (v || '').toUpperCase(); + }, + + lc: function(v) { + return (v || '').toLowerCase(); + }, + + s: function(v) { + return (v == 1) ? '' : 's'; + }, + + length: function(v) { + return (v || '').length; + }, + + trim: function(v) { + return trim(v); + }, + + h: (function() { + var LUT = { + '"': '"', + "'": ''', + '>': '>', + '<': '<', + '&': '&' + }; + + return function(v) { + if (v === undefined || v === null) + return ''; + + return v.toString().replace(/(['"<>&])/g, function(s) { + return LUT[s]; + }); + }; + })() + }; + + var RES = { + actions: /%\{\s*([^\s\|\}]+)\s*((\s*\|(\s*[^\s\|\}]+)+)*)\s*\}|([^%]+|%)/g, + filter: /(\S+)((\s*\S+)*)\s*/, + delim_filters: /\s*\|\s*/, + delim_args: /\s+/ + }; + + function parse_template(s) { + var r = []; + + scan(s, RES.actions, function(m) { + if (m[1]) { + // action + r.push({ + type: 'action', + key: m[1], + filters: parse_filters(m[2]) + }); + } else { + // text + r.push({ + type: 'text', + text: m[5] + }); + } + }); + + return r; + } + + function parse_filters(filters) { + var r = []; + + each(filters.split(RES.delim_filters), function(f) { + f = trim(f); + if (!f) + return; + + var m = f.match(RES.filter); + if (!m) + throw new Error('invalid filter: ' + f); + + var as = trim(m[2]); + + r.push({ + name: m[1], + args: as.length ? as.split(RES.delim_args) : [] + }); + }); + + return r; + } + + function init(s) { + this.s = s; + this.actions = parse_template(s); + }; + + function run(o) { + var i, l, f, fs, me = this; + + // debug + // print(JSON.stringify(this.actions)); + + return map(this.actions, function(row) { + if (row.type == 'text') { + return row.text; + } else if (row.type == 'action') { + if (!row.key in o) + throw new Error('missing key: ' + row.key) + + return reduce(row.filters, function(r, f) { + return FILTERS[f.name](r, f.args, o, this); + }, o[row.key]); + } else { + /* never reached */ + throw new Error('BUG: invalid type: ' + row.type); + } + }).join(''); + } + + function get_inline_template(key) { + // get script element + var e = document.getElementById(key); + if (!e) + throw new Error('unknown inline template key: ' + key); + + // return result + return e.innerText || ''; + } + + // declare constructor + var T = init; + + // declare run method + T.prototype.run = run; + + // declare cache constructor + T.Cache = function(templates, try_dom) { + this.templates = templates; + this.try_dom = !!try_dom; + this.cache = {}; + }; + + // cache run method + T.Cache.prototype.run = function(key, args) { + if (!(key in this.cache)) { + var s = null; + + if (key in this.templates) { + // get template from constructor templates + s = this.templates[key].join(''); + } else if (this.try_dom) { + // get source from inline script tag + s = get_inline_template(key); + } else { + throw new Error('unknown key: ' + key); + } + + // cache template + this.cache[key] = new T(s); + } + + // run template + return this.cache[key].run(args); + }; + + // declare domcache constructor + T.DOMCache = function() { + this.cache = {}; + }; + + // domcache run method + T.DOMCache.prototype.run = function(key, args) { + if (!(key in this.cache)) + this.cache[key] = new T(get_inline_template(key)); + + // run template + return this.cache[key].run(args); + }; + + // create DOMCache singleton + T.dom = new T.DOMCache(); + + // expose filters and version + T.FILTERS = FILTERS; + T.VERSION = VERSION; + + // add singleton run + T.run = function(s, o) { + return new T(s).run(o); + } + + // expose interface + return T; +}()); + +/* +You automagically generate the following files: + + * luigi-template.min.js (minified luigi-template.js), + * readme.txt (Markdown-formatted documentation), and + * readme.html (HTML-formatted documentation) + +by using this command: + + grep ^build: luigi-template.js | sed 's/^build://' | ruby + +(Requires jsmin, ruby, and markdown). + +build: # generate readme.txt +build: File.write('readme.txt', File.read('luigi-template.js').match(%r{ +build: # match first opening comment +build: ^/\*\*(.*?)\* / +build: +build: # match text +build: (.*?) +build: +build: # match first closing comment +build: # (note: don't change " /" to "/" or else) +build: \* / +build: }mx)[1].split(/\n/).map { |line| +build: # strip leading asterisks +build: line.gsub(/^ \* ?/, '') +build: }.join("\n").strip) +build: +build: # generate readme.html +build: `markdown < readme.txt > readme.html` +build: +build: # make luigi-template.min.js +build: `jsmin < luigi-template.js > luigi-template.min.js` +*/ diff --git a/js/test.js b/js/test.js new file mode 100644 index 0000000..0fedfcd --- /dev/null +++ b/js/test.js @@ -0,0 +1,58 @@ + +load('luigi-template.js'); + +// define custom template filter +function custom_filter(v) { + return "foo" + v + "bar"; +} + +function custom_filter_with_args(v, args) { + var i, l, r = [v]; + + for (i = 0, l = args.length; i < l; i++) + r.push(args[i]); + + return r.join(' and '); +} + +// add custom template filters +LuigiTemplate.FILTERS.custom = custom_filter; +LuigiTemplate.FILTERS.custom_args = custom_filter_with_args; + +// build template string +var template_str = [ + // test basic templates + "%{greet}, %{name}!", + + // test filters and filters with parameters + "Your name uppercase is: %{name|uc}", + + // test custom filter + "Your custom filtered name is: %{name|custom}", + + // test custom filter with arguments + "Your custom_args name is: %{name|custom_args foo bar baz}", + + // test whitespace in filters + "random test: %{name | lc }", + + // test pluralize filter + 'pluralize test (0): %{count_0} item%{count_0 | s}', + 'pluralize test (1): %{count_1} item%{count_1 | s}', + 'pluralize test (10): %{count_10} item%{count_10 | s}', + + // terminating newline + '' +].join("\n"); + +// build template +var t = new LuigiTemplate(template_str); + +// print results +print(t.run({ + greet: 'hello', + name: 'paul', + count_0: 0, + count_1: 1, + count_10: 10 +})); |