Skip to content

Instantly share code, notes, and snippets.

@datchley datchley/README.md
Last active Mar 23, 2019

Embed
What would you like to do?
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

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.