summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/luigi-template.js379
-rw-r--r--js/test.js58
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 = {
+ '"': '&quot;',
+ "'": '&apos;',
+ '>': '&gt;',
+ '<': '&lt;',
+ '&': '&amp;'
+ };
+
+ 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
+}));