// json-templates // Simple templating within JSON structures. // // Created by Curran Kelleher and Chrostophe Serafin. // Contributions from Paul Brewer and Javier Blanco Martinez. import objectPath from 'object-path'; import dedupe from 'dedupe'; // An enhanced version of `typeof` that handles arrays and dates as well. function type(value) { let valueType: string = typeof value; if (Array.isArray(value)) { valueType = 'array'; } else if (value instanceof Date) { valueType = 'date'; } else if (value === null) { valueType = 'null'; } return valueType; } // Constructs a parameter object from a match result. // e.g. "['{{foo}}']" --> { key: "foo" } // e.g. "['{{foo:bar}}']" --> { key: "foo", defaultValue: "bar" } function Parameter(match) { let param; const matchValue = match.substr(2, match.length - 4).trim(); const i = matchValue.indexOf(':'); if (i !== -1) { param = { key: matchValue.substr(0, i), defaultValue: matchValue.substr(i + 1), }; } else { param = { key: matchValue }; } return param; } // Constructs a template function with deduped `parameters` property. function Template(fn, parameters) { // Paul Brewer Dec 2017 add deduplication call, use only key property to eliminate Object.assign(fn, { parameters: dedupe(parameters, (item) => item.key), }); return fn; } // Parses the given template object. // // Returns a function `template(context)` that will "fill in" the template // with the context object passed to it. // // The returned function has a `parameters` property, // which is an array of parameter descriptor objects, // each of which has a `key` property and possibly a `defaultValue` property. export function parse(value) { switch (type(value)) { case 'string': return parseString(value); case 'object': return parseObject(value); case 'array': return parseArray(value); default: return Template(function () { return value; }, []); } } // Parses leaf nodes of the template object that are strings. // Also used for parsing keys that contain templates. const parseString = (() => { // This regular expression detects instances of the // template parameter syntax such as {{foo}} or {{foo:someDefault}}. const regex = /{{(\w|:|[\s-+.,@///()?=*_$])+}}/g; return (str) => { let parameters = []; let templateFn = (context) => str; const matches = str.match(regex); if (matches) { parameters = matches.map(Parameter); templateFn = (context) => { context = context || {}; return matches.reduce((result, match, i) => { const parameter = parameters[i]; let value = objectPath.get(context, parameter.key); const type = typeof value; if (type === 'undefined') { if (typeof parameter.defaultValue === 'undefined') { return value; } value = parameter.defaultValue; } if (type === 'function') { value = value(); } if (type === 'object') { return value; } // Accommodate numbers as values. if (matches.length === 1 && str.startsWith('{{') && str.endsWith('}}')) { return value; } return result.replace(match, value == null ? '' : value); }, str); }; } return Template(templateFn, parameters); }; })(); // Parses non-leaf-nodes in the template object that are objects. function parseObject(object) { const children = Object.keys(object).map((key) => ({ keyTemplate: parseString(key), valueTemplate: parse(object[key]), })); const templateParameters = children.reduce( (parameters, child) => parameters.concat(child.valueTemplate.parameters, child.keyTemplate.parameters), [], ); const templateFn = (context) => { return children.reduce((newObject, child) => { newObject[child.keyTemplate(context)] = child.valueTemplate(context); return newObject; }, {}); }; return Template(templateFn, templateParameters); } // Parses non-leaf-nodes in the template object that are arrays. function parseArray(array) { const templates = array.map(parse); const templateParameters = templates.reduce((parameters, template) => parameters.concat(template.parameters), []); const templateFn = (context) => templates.map((template) => template(context)); return Template(templateFn, templateParameters); }