/*! dustjs-helpers - v1.3.0 * https://github.com/linkedin/dustjs-helpers * Copyright (c) 2014 Aleksander Williams; Released under the MIT License */ (function(dust){ //using the built in logging method of dust when accessible var _log = dust.log ? function(mssg) { dust.log(mssg, "INFO"); } : function() {}; function isSelect(context) { var value = context.current(); return typeof value === "object" && value.isSelect === true; } // Utility method : toString() equivalent for functions function jsonFilter(key, value) { if (typeof value === "function") { //to make sure all environments format functions the same way return value.toString() //remove all leading and trailing whitespace .replace(/(^\s+|\s+$)/mg, '') //remove new line characters .replace(/\n/mg, '') //replace , and 0 or more spaces with ", " .replace(/,\s*/mg, ', ') //insert space between ){ .replace(/\)\{/mg, ') {') ; } return value; } // Utility method: to invoke the given filter operation such as eq/gt etc function filter(chunk, context, bodies, params, filterOp) { params = params || {}; var body = bodies.block, actualKey, expectedValue, filterOpType = params.filterOpType || ''; // when @eq, @lt etc are used as standalone helpers, key is required and hence check for defined if ( typeof params.key !== "undefined") { actualKey = dust.helpers.tap(params.key, chunk, context); } else if (isSelect(context)) { actualKey = context.current().selectKey; // supports only one of the blocks in the select to be selected if (context.current().isResolved) { filterOp = function() { return false; }; } } else { _log("No key specified for filter in:" + filterOpType + " helper "); return chunk; } expectedValue = dust.helpers.tap(params.value, chunk, context); // coerce both the actualKey and expectedValue to the same type for equality and non-equality compares if (filterOp(coerce(expectedValue, params.type, context), coerce(actualKey, params.type, context))) { if (isSelect(context)) { context.current().isResolved = true; } // we want helpers without bodies to fail gracefully so check it first if(body) { return chunk.render(body, context); } else { _log("No key specified for filter in:" + filterOpType + " helper "); return chunk; } } else if (bodies['else']) { return chunk.render(bodies['else'], context); } return chunk; } function coerce (value, type, context) { if (value) { switch (type || typeof(value)) { case 'number': return +value; case 'string': return String(value); case 'boolean': { value = (value === 'false' ? false : value); return Boolean(value); } case 'date': return new Date(value); case 'context': return context.get(value); } } return value; } var helpers = { // Utility helping to resolve dust references in the given chunk // uses the Chunk.render method to resolve value /* Reference resolution rules: if value exists in JSON: "" or '' will evaluate to false, boolean false, null, or undefined will evaluate to false, numeric 0 evaluates to true, so does, string "0", string "null", string "undefined" and string "false". Also note that empty array -> [] is evaluated to false and empty object -> {} and non-empty object are evaluated to true The type of the return value is string ( since we concatenate to support interpolated references if value does not exist in JSON and the input is a single reference: {x} dust render emits empty string, and we then return false if values does not exist in JSON and the input is interpolated references : {x} < {y} dust render emits < and we return the partial output */ "tap": function(input, chunk, context) { // return given input if there is no dust reference to resolve // dust compiles a string/reference such as {foo} to a function if (typeof input !== "function") { return input; } var dustBodyOutput = '', returnValue; //use chunk render to evaluate output. For simple functions result will be returned from render call, //for dust body functions result will be output via callback function returnValue = chunk.tap(function(data) { dustBodyOutput += data; return ''; }).render(input, context); chunk.untap(); //assume it's a simple function call if return result is not a chunk if (returnValue.constructor !== chunk.constructor) { //use returnValue as a result of tap return returnValue; } else if (dustBodyOutput === '') { return false; } else { return dustBodyOutput; } }, "sep": function(chunk, context, bodies) { var body = bodies.block; if (context.stack.index === context.stack.of - 1) { return chunk; } if(body) { return bodies.block(chunk, context); } else { return chunk; } }, "idx": function(chunk, context, bodies) { var body = bodies.block; if(body) { return bodies.block(chunk, context.push(context.stack.index)); } else { return chunk; } }, /** * contextDump helper * @param key specifies how much to dump. * "current" dumps current context. "full" dumps the full context stack. * @param to specifies where to write dump output. * Values can be "console" or "output". Default is output. */ "contextDump": function(chunk, context, bodies, params) { var p = params || {}, to = p.to || 'output', key = p.key || 'current', dump; to = dust.helpers.tap(to, chunk, context); key = dust.helpers.tap(key, chunk, context); if (key === 'full') { dump = JSON.stringify(context.stack, jsonFilter, 2); } else { dump = JSON.stringify(context.stack.head, jsonFilter, 2); } if (to === 'console') { _log(dump); return chunk; } else { return chunk.write(dump); } }, /** if helper for complex evaluation complex logic expressions. Note : #1 if helper fails gracefully when there is no body block nor else block #2 Undefined values and false values in the JSON need to be handled specially with .length check for e.g @if cond=" '{a}'.length && '{b}'.length" is advised when there are chances of the a and b been undefined or false in the context #3 Use only when the default ? and ^ dust operators and the select fall short in addressing the given logic, since eval executes in the global scope #4 All dust references are default escaped as they are resolved, hence eval will block malicious scripts in the context Be mindful of evaluating a expression that is passed through the unescape filter -> |s @param cond, either a string literal value or a dust reference a string literal value, is enclosed in double quotes, e.g. cond="2>3" a dust reference is also enclosed in double quotes, e.g. cond="'{val}'' > 3" cond argument should evaluate to a valid javascript expression **/ "if": function( chunk, context, bodies, params ){ var body = bodies.block, skip = bodies['else']; if( params && params.cond){ var cond = params.cond; cond = dust.helpers.tap(cond, chunk, context); // eval expressions with given dust references if(eval(cond)){ if(body) { return chunk.render( bodies.block, context ); } else { _log("Missing body block in the if helper!"); return chunk; } } if(skip){ return chunk.render( bodies['else'], context ); } } // no condition else { _log("No condition given in the if helper!"); } return chunk; }, /** * math helper * @param key is the value to perform math against * @param method is the math method, is a valid string supported by math helper like mod, add, subtract * @param operand is the second value needed for operations like mod, add, subtract, etc. * @param round is a flag to assure that an integer is returned */ "math": function ( chunk, context, bodies, params ) { //key and method are required for further processing if( params && typeof params.key !== "undefined" && params.method ){ var key = params.key, method = params.method, // operand can be null for "abs", ceil and floor operand = params.operand, round = params.round, mathOut = null, operError = function(){ _log("operand is required for this math method"); return null; }; key = dust.helpers.tap(key, chunk, context); operand = dust.helpers.tap(operand, chunk, context); // TODO: handle and tests for negatives and floats in all math operations switch(method) { case "mod": if(operand === 0 || operand === -0) { _log("operand for divide operation is 0/-0: expect Nan!"); } mathOut = parseFloat(key) % parseFloat(operand); break; case "add": mathOut = parseFloat(key) + parseFloat(operand); break; case "subtract": mathOut = parseFloat(key) - parseFloat(operand); break; case "multiply": mathOut = parseFloat(key) * parseFloat(operand); break; case "divide": if(operand === 0 || operand === -0) { _log("operand for divide operation is 0/-0: expect Nan/Infinity!"); } mathOut = parseFloat(key) / parseFloat(operand); break; case "ceil": mathOut = Math.ceil(parseFloat(key)); break; case "floor": mathOut = Math.floor(parseFloat(key)); break; case "round": mathOut = Math.round(parseFloat(key)); break; case "abs": mathOut = Math.abs(parseFloat(key)); break; default: _log("method passed is not supported"); } if (mathOut !== null){ if (round) { mathOut = Math.round(mathOut); } if (bodies && bodies.block) { // with bodies act like the select helper with mathOut as the key // like the select helper bodies['else'] is meaningless and is ignored return chunk.render(bodies.block, context.push({ isSelect: true, isResolved: false, selectKey: mathOut })); } else { // self closing math helper will return the calculated output return chunk.write(mathOut); } } else { return chunk; } } // no key parameter and no method else { _log("Key is a required parameter for math helper along with method/operand!"); } return chunk; }, /** select helper works with one of the eq/ne/gt/gte/lt/lte/default providing the functionality of branching conditions @param key, ( required ) either a string literal value or a dust reference a string literal value, is enclosed in double quotes, e.g. key="foo" a dust reference may or may not be enclosed in double quotes, e.g. key="{val}" and key=val are both valid @param type (optional), supported types are number, boolean, string, date, context, defaults to string **/ "select": function(chunk, context, bodies, params) { var body = bodies.block; // key is required for processing, hence check for defined if( params && typeof params.key !== "undefined"){ // returns given input as output, if the input is not a dust reference, else does a context lookup var key = dust.helpers.tap(params.key, chunk, context); // bodies['else'] is meaningless and is ignored if( body ) { return chunk.render(bodies.block, context.push({ isSelect: true, isResolved: false, selectKey: key })); } else { _log("Missing body block in the select helper "); return chunk; } } // no key else { _log("No key given in the select helper!"); } return chunk; }, /** eq helper compares the given key is same as the expected value It can be used standalone or in conjunction with select for multiple branching @param key, The actual key to be compared ( optional when helper used in conjunction with select) either a string literal value or a dust reference a string literal value, is enclosed in double quotes, e.g. key="foo" a dust reference may or may not be enclosed in double quotes, e.g. key="{val}" and key=val are both valid @param value, The expected value to compare to, when helper is used standalone or in conjunction with select @param type (optional), supported types are number, boolean, string, date, context, defaults to string Note : use type="number" when comparing numeric **/ "eq": function(chunk, context, bodies, params) { if(params) { params.filterOpType = "eq"; } return filter(chunk, context, bodies, params, function(expected, actual) { return actual === expected; }); }, /** ne helper compares the given key is not the same as the expected value It can be used standalone or in conjunction with select for multiple branching @param key, The actual key to be compared ( optional when helper used in conjunction with select) either a string literal value or a dust reference a string literal value, is enclosed in double quotes, e.g. key="foo" a dust reference may or may not be enclosed in double quotes, e.g. key="{val}" and key=val are both valid @param value, The expected value to compare to, when helper is used standalone or in conjunction with select @param type (optional), supported types are number, boolean, string, date, context, defaults to string Note : use type="number" when comparing numeric **/ "ne": function(chunk, context, bodies, params) { if(params) { params.filterOpType = "ne"; return filter(chunk, context, bodies, params, function(expected, actual) { return actual !== expected; }); } return chunk; }, /** lt helper compares the given key is less than the expected value It can be used standalone or in conjunction with select for multiple branching @param key, The actual key to be compared ( optional when helper used in conjunction with select) either a string literal value or a dust reference a string literal value, is enclosed in double quotes, e.g. key="foo" a dust reference may or may not be enclosed in double quotes, e.g. key="{val}" and key=val are both valid @param value, The expected value to compare to, when helper is used standalone or in conjunction with select @param type (optional), supported types are number, boolean, string, date, context, defaults to string Note : use type="number" when comparing numeric **/ "lt": function(chunk, context, bodies, params) { if(params) { params.filterOpType = "lt"; return filter(chunk, context, bodies, params, function(expected, actual) { return actual < expected; }); } }, /** lte helper compares the given key is less or equal to the expected value It can be used standalone or in conjunction with select for multiple branching @param key, The actual key to be compared ( optional when helper used in conjunction with select) either a string literal value or a dust reference a string literal value, is enclosed in double quotes, e.g. key="foo" a dust reference may or may not be enclosed in double quotes, e.g. key="{val}" and key=val are both valid @param value, The expected value to compare to, when helper is used standalone or in conjunction with select @param type (optional), supported types are number, boolean, string, date, context, defaults to string Note : use type="number" when comparing numeric **/ "lte": function(chunk, context, bodies, params) { if(params) { params.filterOpType = "lte"; return filter(chunk, context, bodies, params, function(expected, actual) { return actual <= expected; }); } return chunk; }, /** gt helper compares the given key is greater than the expected value It can be used standalone or in conjunction with select for multiple branching @param key, The actual key to be compared ( optional when helper used in conjunction with select) either a string literal value or a dust reference a string literal value, is enclosed in double quotes, e.g. key="foo" a dust reference may or may not be enclosed in double quotes, e.g. key="{val}" and key=val are both valid @param value, The expected value to compare to, when helper is used standalone or in conjunction with select @param type (optional), supported types are number, boolean, string, date, context, defaults to string Note : use type="number" when comparing numeric **/ "gt": function(chunk, context, bodies, params) { // if no params do no go further if(params) { params.filterOpType = "gt"; return filter(chunk, context, bodies, params, function(expected, actual) { return actual > expected; }); } return chunk; }, /** gte helper, compares the given key is greater than or equal to the expected value It can be used standalone or in conjunction with select for multiple branching @param key, The actual key to be compared ( optional when helper used in conjunction with select) either a string literal value or a dust reference a string literal value, is enclosed in double quotes, e.g. key="foo" a dust reference may or may not be enclosed in double quotes, e.g. key="{val}" and key=val are both valid @param value, The expected value to compare to, when helper is used standalone or in conjunction with select @param type (optional), supported types are number, boolean, string, date, context, defaults to string Note : use type="number" when comparing numeric **/ "gte": function(chunk, context, bodies, params) { if(params) { params.filterOpType = "gte"; return filter(chunk, context, bodies, params, function(expected, actual) { return actual >= expected; }); } return chunk; }, // to be used in conjunction with the select helper // TODO: fix the helper to do nothing when used standalone "default": function(chunk, context, bodies, params) { // does not require any params if(params) { params.filterOpType = "default"; } return filter(chunk, context, bodies, params, function(expected, actual) { return true; }); }, /** * size helper prints the size of the given key * Note : size helper is self closing and does not support bodies * @param key, the element whose size is returned */ "size": function( chunk, context, bodies, params ) { var key, value=0, nr, k; params = params || {}; key = params.key; if (!key || key === true) { //undefined, null, "", 0 value = 0; } else if(dust.isArray(key)) { //array value = key.length; } else if (!isNaN(parseFloat(key)) && isFinite(key)) { //numeric values value = key; } else if (typeof key === "object") { //object test //objects, null and array all have typeof ojbect... //null and array are already tested so typeof is sufficient http://jsperf.com/isobject-tests nr = 0; for(k in key){ if(Object.hasOwnProperty.call(key,k)){ nr++; } } value = nr; } else { value = (key + '').length; //any other value (strings etc.) } return chunk.write(value); } }; for (var key in helpers) { dust.helpers[key] = helpers[key]; } if(typeof exports !== 'undefined') { module.exports = dust; } })(typeof exports !== 'undefined' ? require('dustjs-linkedin') : dust);