Skip to content

Instantly share code, notes, and snippets.

@datchley
Last active March 23, 2019 21:00
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save datchley/914d25f204d8801df298 to your computer and use it in GitHub Desktop.
Save datchley/914d25f204d8801df298 to your computer and use it in GitHub Desktop.
Micro templating library for javascript

Micro-Template: README.md

Valid template expressions:

  • expressions can reference any property on the passed in context, including nested properties and indexed access to array properties.
     {{ name }}
     {{ obj.name }}
     {{ obj.name[0] }}
  • properties on the context can be passed through filter functions defined using registerHelpers({...}). Filter functions always get the property value as the first argument and can take any number of additional, static string or number arguments.
     {{ name | filter }}
     {{ name | filter 'string' 1234 }}
  • Filter functions can be chained in succession using '|'s
     {{ name | filter | filter }}
  • A filter function may be called directly as opposed to referencing a context property first.
     {{ filter | filter | ... }}

Valid template control statements and blocks:

A block/control statement starts with '{{#..}}' and ends with '{{/...}}', vs the usual '{{' for interpolation of values. Each 'expression' is a valid template interpolation expression as noted above.

  • if/else/elsif conditional blocks
{{#if expression }}
// ...
{{/if}}

or, with an else or elsif

{{#if expression }}
// ...
{{#elsif expression }}
// ...
{{#else}}
// ...
{{/if}}
  • for/in iterative blocks for iterating on a given context property using a supplied name for each itemthat is an array
{{#for item in items}}
//...
    <li>{{ item.name }} - {{ item.age }}</li>
{{/for}}

or, using the foreach to iterate where the item context is given for you

{{#foreach list}}
    <li>{{ name }} - {{age}}</li>
{{/foreach}}
//
// Micro Templating library for compiling template functions
//
var template = (function() {
var _cache = {}, // template cache
_helpers = { // register helpers on this object
'__noop': function(){}
};
/**
* registerHelpers - registers helper functions that can act as filters/passthrus
* on context data. Will not overwrite an already existing helper with the same name.
* @param {Object} cfg - an object whose keys are the helper's function name, and value
* the function definition.
*/
function registerHelpers(cfg) {
var names = Object.keys(cfg);
names.forEach(function(helper) {
if (!_helpers[helper]) {
_helpers[helper] = cfg[helper];
}
});
}
/**
* dequote - helper function to remove literal quotes around any string arguments
*/
function dequote() {
var args = [].slice.call(arguments);
return args.map(function(a){ return (typeof a === 'string') ? a.replace(/^['"]|['"]$/g,'') : a; });
}
/**
* tokenizer - simple method to tokenize a template string, returning
* a tokenizer (simple iterator) with a 'next()' and 'peek()' function to retrieve token objects
* that have a type and a value, or false when done.
* @param {String} s - a template string to be tokenized
* @return {Object} - an tokenizer object with a next and peek method
*/
function tokenizer(s) {
var index = 0,
read = function(s, start) {
var part = s.slice(start),
token;
if (!part.length){
return false;
}
if (part[0] == '{' && part[1] == '{') {
token = {type: 'stmt', value: part.slice(0, part.indexOf('}}')+2) };
if (/^{{\s?#/.test(token.value))
token.type = 'block-start';
else if (/^{{\s?\//.test(token.value))
token.type = 'block-end';
else
token.type = 'exp';
}
else {
token = (part.indexOf('{{') !== -1) ?
{ type: 'text', value: part.slice(0, part.indexOf('{{')) }:
{ type: 'text', value: part };
}
return token;
};
return {
next: function() {
var token = read(s, index);
index += (token) ? token.value.length : index;
if (token) {
token.value = token.value.replace(/{{|}}/g,'');
}
return token;
},
peek: function(){
return read(s, index);
}
};
}
/**
* interpolate - Given an expression, which is a valid template expression of a value or
* function followed by one or more filters joined with '|'s, will evaluate
* the expression using the given context data and provided helper
* functions.
*
* Valid template expressions:
*
* - expressions can reference any property on the passed in context, including
* nested properties and indexed access to array properties.
*
* {{ name }}
* {{ obj.name }}
* {{ obj.name[0] }}
*
* - properties on the context can be passed through filter functions defined using
* registerHelpers({...}). Filter functions always get the property value as the first
* argument and can take any number of additional, static string or number arguments.
*
* {{ name | filter }}
* {{ name | filter 'string' 1234 }}
*
* - Filter functions can be chained in succession using '|'s
*
* {{ name | filter | filter }}
*
* - A filter function may be called directly as opposed to referencing a context property first.
*
* {{ filter | filter | ... }}
*
* @param {String} expression - a valid template value expression, ie (value | filter | ...)
* @param {Object} context - the data object to use in evaluating the expression
* @param {Object} helpers - a map of helper functions, each property key is a function name
* @returns {Mixed} - the resulting value of the expression
*/
function interpolate(expression, context, helpers) {
var lexer = expression.trim().split(/\s*\|\s*/),
exp = lexer.shift(),
value = exp.split(/[\.\[\]]/)
.filter(function(s){ return !!s; })
.reduce(function(prev, cur) {
return prev[parseInt(cur,10) || cur];
}, context);
// if no value here, it doesn't exist in the context, but
// might be a direct call to a helper function
if (!value) {
var parts = exp.split(/\s+/),
fn = parts.shift();
if (helpers[fn]) {
parts = dequote.apply(null, parts);
value = helpers[fn].apply(null, parts);
}
}
while (lexer.length) {
var exp = lexer.shift().split(/\s+/),
filter = helpers[exp.shift() || '__noop'],
args = exp.length ? [value].concat(exp) : [value];
// strip quotes from string literals
args = dequote.apply(null, args);
value = filter.apply(null, args);
}
return value;
}
/**
* compile - Compile's a template into a reusable function that
* taks a data context object and returns a fully evaluated version
* of the template against that context.
*
* @param {String} template - the string template to compile
* @param {Object} [data] - optional data context to curry in the returned function
* @returns {Function} - the template rendering function, optionally pre-bound to
* a data context.
*/
function compile(template, data) {
// strip newlines from template
template = template.replace(/(\r\n|\n|\r)/gm,'');
// return cached if already compiled
if (_cache[template]) {
return cache[template];
}
// tokenizer (iterator) for template
var tokens = tokenizer(template),
// local bind context for generated function
env = {
'interpolate': interpolate,
'helpers': _helpers
},
// generated function statement stack
fn = [ 'var p = [], self = this;' ],
// utility for evaluating an expression in generated function
_eval = function(exp) {
return 'self.interpolate("' + exp + '", context, self.helpers)';
};
while ((token = tokens.next())) {
if (token.type == 'block-start') {
var stmt = token.value.match(/^#(?:\s+)?([^}]+)/), // separate tag-name from statement expression
parts = stmt[1].split(' '), // split statement expression into parts
tagname = parts[0], // the tag name (if, for, foreach,...)
rest = parts.slice(1).join(' '); // rest of the expression (val | filter | ...)
switch(tagname) {
case 'if':
fn.push('if (' + _eval(rest) + ') {');
break;
case 'else':
fn.push('} else {');
break;
case 'elsif':
fn.push('} else if (' + _eval(rest) + ') {');
break;
case 'for':
fn.push(_eval(parts[3]) + '.forEach(function(obj, index){');
fn.push('var context = { "' + parts[1] + '": obj };');
break;
case 'foreach':
fn.push(_eval(parts[1]) + '.forEach(function(obj, index){');
fn.push('var context = obj;');
}
}
else if (token.type == 'block-end') {
var stmt = token.value.match(/^\/(?:\s+)?([^}]+)/), // separate end tag-name from remaining
etagname = stmt[1]; // end tag-name
switch(etagname) {
case 'if':
fn.push('}');
break;
case 'for':
case 'foreach':
fn.push('});');
break;
}
}
else if (token.type === 'exp') {
fn.push('p.push(' + _eval(token.value) + ');');
}
else {
fn.push("p.push('" + token.value + "');");
}
}
fn.push('return p.join(\'\');');
// cache the generated function agains the stripped template as a key
// return the function, currying any data context if given one
if (data) {
_cache[template] = (new Function('context', fn.join(''))).bind(env, data);
}
else {
_cache[template] = (new Function('context', fn.join(''))).bind(env);
}
return _cache[template];
}
// return an object exposing the API
return {
'compile': compile,
'registerHelpers': registerHelpers
};
})();
//
// Unit Test
//
// Register some helper functions to use in templates
template.registerHelpers({
uppercase: function(val){ return val.toString().toUpperCase(); },
reverse: function(val) { return val.split('').reverse().join(''); },
equals: function(val, comp) { console.log(">> args: ", arguments); return val == comp; }
});
// Our handy, dandy template
var btemplate = 'Name: {{name | uppercase}} <br/>' +
'{{#if show}}' +
'<span class="xxx">Language: {{language | reverse}}</span>' +
'{{/if}}' +
'<ul>' +
'{{#for project in projects}}' +
'<li>{{project.name}} - {{project.date}}</li>' +
'{{/for}}' +
'</ul><ul>' +
'{{#foreach projects}}' +
'<li>{{name}} - {{date}}<br/>' +
'status: ' +
'{{#if status | equals \'active\'}}' +
'<span style="color: green">{{status}}</span>' +
'{{#elsif status | equals \'inactive\'}}' +
'<span style="color: orange">{{status}}</span>' +
'{{#else}}' +
'<span>no status</span>' +
'{{/if}}' +
'{{/foreach}}' +
'</ul>' +
'<span>show: {{show}}</span>';
// Compile our template function...
var F = template.compile(btemplate);
var _data = { name: 'Dave',
language: 'Javascript',
projects: [
{ name: 'Primary', date: '10/15/2015', status: 'active' },
{ name: 'Secondary', date: '09/08/2014', status: 'inactive' },
{ name: 'Tertiary', date: '07/04/2015', status: undefined }
],
show: true
};
// Call our template function with our data and slap it in the body
$(document.body).append(F(_data));
@h3
Copy link

h3 commented May 8, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment