here's a standalone script templateRender.js (~200 sloc) that can solve your needs in both browser/nodejs env.
/*
* templateRender.js
* solution to [nodejs] json-templater api in node/express
* solution to Subject: [nodejs] json-templater api in node/express
*
* feel free to edit it to suit your needs
*/
/*jslint
bitwise: true,
browser: true,
maxerr: 8,
maxlen: 100,
node: true,
nomen: true,
regexp: true,
stupid: true
*/
'use strict';
var local;
local = {};
local.templateRender = function (template, dict, options) {
/*
* this function will render the template with the given dict
* example usage:
* local.templateRender('foo {{
aa.bb}}', { aa: { bb: 'bar' } } -> ‘foo bar’
*/
var argList, getValue, match, renderPartial, rgx, tryCatch, value;
dict = dict || {};
options = options || {};
getValue = function (key) {
argList = key.split(' ');
value = dict;
if (argList[0] === '#this/') {
return;
}
// iteratively lookup nested values in the dict
argList[0].split('.').forEach(function (key) {
value = value && value[key];
});
return value;
};
renderPartial = function (match0, helper, key, partial) {
switch (helper) {
case 'each':
case 'eachTrimRightComma':
value = getValue(key);
value = Array.isArray(value)
? value.map(function (dict) {
// recurse with partial
return local.templateRender(partial, dict, options);
}).join('')
: '';
// remove trailing-comma from last element
if (helper === 'eachTrimRightComma') {
value = value.trimRight().replace((/,$/), '');
}
return value;
case 'if':
partial = partial.split('{{#unless ' + key + '}}');
partial = getValue(key)
? partial[0]
// handle 'unless' case
: partial.slice(1).join('{{#unless ' + key + '}}');
// recurse with partial
return local.templateRender(partial, dict, options);
case 'unless':
return getValue(key)
? ''
// recurse with partial
: local.templateRender(partial, dict, options);
default:
// recurse with partial
return match0[0] + local.templateRender(match0.slice(1), dict, options);
}
};
tryCatch = function (fnc, message) {
/*
* this function will prepend the message to errorCaught
*/
try {
return fnc();
} catch (errorCaught) {
errorCaught.message = message + errorCaught.message;
throw errorCaught;
}
};
// render partials
rgx = (/\{\{#(\w+) ([^}]+?)\}\}/g);
template = template || '';
for (match = rgx.exec(template); match; match = rgx.exec(template)) {
rgx.lastIndex += 1 - match[0].length;
template = template.replace(
new RegExp('\\{\\{#(' + match[1] + ') (' + match[2] +
')\\}\\}([\\S\\s]*?)\\{\\{/' + match[1] + ' ' + match[2] +
'\\}\\}'),
renderPartial
);
}
// search for keys in the template
return template.replace((/\{\{[^}]+?\}\}/g), function (match0) {
var markdownToHtml, notHtmlSafe;
notHtmlSafe = options.notHtmlSafe;
return tryCatch(function () {
getValue(match0.slice(2, -2));
if (value === undefined) {
return match0;
}
argList.slice(1).forEach(function (arg) {
switch (arg) {
case 'alphanumeric':
value = value.replace((/\W/g), '_');
break;
case 'decodeURIComponent':
value = decodeURIComponent(value);
break;
case 'encodeURIComponent':
value = encodeURIComponent(value);
break;
case 'jsonStringify':
value = JSON.stringify(value);
break;
case 'jsonStringify4':
value = JSON.stringify(value, null, 4);
break;
case 'markdownSafe':
value = value.replace((/`/g), '\'');
break;
case 'markdownToHtml':
markdownToHtml = true;
break;
case 'notHtmlSafe':
notHtmlSafe = true;
break;
// default to String.prototype[arg]()
default:
value = value[arg]();
break;
}
});
value = String(value);
// default to htmlSafe
if (!notHtmlSafe) {
value = value.replace((/["&'<>]/g), function (match0) {
return '&#x' + match0.charCodeAt(0).toString(16) + ';';
});
}
if (markdownToHtml && typeof local.marked === 'function') {
value = local.marked(value);
}
return value;
}, 'templateRender could not render expression ' + JSON.stringify(match0) + '\n');
});
};
// init test-template
/* jslint-ignore-begin */
local.template = '{\n\
"username": "{{uname}}",\n\
"userId" : "{{uid}}",\n\
"myList": [\n\
{{#eachTrimRightComma myList}}\n\
{\n\
"aa2": {\n\
"bb2": {{
aa.bb jsonStringify}}\n\
}\n\
},\n\
{{/eachTrimRightComma myList}}\n\
]\n\
}';
/* jslint-ignore-end */
// render test-template
local.dict = {
uname: 'john1234',
uid: 1234,
// test nested objects in a list
myList: [
{ aa: { bb: { cc: 'dd' } } },
{ aa: { bb: 'cc' } },
// ignored by templater because missing nested item bb
null,
// ignored by templater because missing nested item bb
{},
// ignored by templater because missing nested item bb
{ aa: {} }
]
};
local.result = local.templateRender(local.template, local.dict, { notHtmlSafe: true });
// validate rendered-result is a valid json-object and
local.result = JSON.parse(local.result);
// print template vs rendered-json-result to stderr
console.error(local.template);
console.error('-> ' + JSON.stringify(local.dict) + ' ->');
console.error(JSON.stringify(local.result, null, 4));
// output:
// {
// "username": "{{uname}}",
// "userId" : "{{uid}}",
// "myList": [
// {{#eachTrimRightComma myList}}
// {
// "aa2": {
// "bb2": {{
aa.bb jsonStringify}}
// }
// },
// {{/eachTrimRightComma myList}}
// ]
// }
// -> {"uname":"john1234","uid":1234,"myList":[{"aa":{"bb":{"cc":"dd"}}},{"aa":{"bb":"cc"}},null,{},{"aa":{}}]} ->
// {
// "username": "john1234",
// "userId": "1234",
// "myList": [
// {
// "aa2": {
// "bb2": {
// "cc": "dd"
// }
// }
// },
// {
// "aa2": {
// "bb2": "cc"
// }
// }
// ]
// }