From 739c14f6cc44836800ce1f5409f0c6d23dcbac3f Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 16:00:40 -0700 Subject: [PATCH 01/12] npm update --- gruntfile.js | 5 +---- package.json | 17 ++++++++--------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/gruntfile.js b/gruntfile.js index 9a78373..4b4cae2 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -135,10 +135,7 @@ module.exports = function (grunt) { }, shell : { testNode: { - command: [ - 'node test/server.js', - 'node test/jasmine-test/server/specRunner.js' - ].join(' && '), + command: 'node test/server.js', options: { stdout: true, failOnError: true diff --git a/package.json b/package.json index 89687c1..67aacca 100644 --- a/package.json +++ b/package.json @@ -44,18 +44,17 @@ "devDependencies": { "dustjs-linkedin": "2.7 - 2.8", "grunt": "~0.4.2", - "grunt-contrib-jshint": "~0.8.0", + "grunt-contrib-jshint": "~0.11.1", "grunt-contrib-jasmine": "~0.8.2", "grunt-template-jasmine-istanbul": "~0.3.3", - "grunt-contrib-connect": "~0.5.0", - "grunt-contrib-watch": "~0.5.3", - "grunt-contrib-uglify": "~0.3.0", - "grunt-contrib-copy": "~0.5.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-bump": "~0.0.13", + "grunt-contrib-connect": "~0.9.0", + "grunt-contrib-watch": "~0.6.1", + "grunt-contrib-uglify": "~0.8.0", + "grunt-contrib-copy": "~0.8.0", + "grunt-contrib-clean": "~0.6.0", + "grunt-bump": "~0.3.0", "grunt-github-changes": "~0.0.6", - "grunt-shell": "~0.6.3", - "jasmine-node": "~1.13.0" + "grunt-shell": "~1.1.2" }, "license": "MIT", "engine": { From b7d7b560e1a7c4b7f2293c921cded66ef3b8ad69 Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 16:00:50 -0700 Subject: [PATCH 02/12] sync jshint from dust core --- .jshintrc | 82 +++++++++++++++++++------- test/jasmine-test/spec/helpersTests.js | 3 +- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/.jshintrc b/.jshintrc index e66f0ed..73f05f3 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,23 +1,63 @@ { - "curly": true, - "eqeqeq": true, - "immed": true, - "latedef": true, - "newcap": true, - "noarg": true, - "sub": true, - "undef": true, - "unused": false, - "boss": true, - "eqnull": true, - "browser": true, - "evil": true, - "globals": { - "dust": true, - "require": true, - "module": true, - "define": true, - "console": true, - "__dirname": true - } + "bitwise" : true, + "curly" : true, + "eqeqeq" : true, + "forin" : false, + "immed" : true, + "latedef" : true, + "newcap" : true, + "noarg" : true, + "noempty" : true, + "nonew" : true, + "plusplus" : false, + "regexp" : true, + "undef" : true, + "strict" : false, + "trailing" : true, + + "asi" : false, + "boss" : false, + "debug" : false, + "eqnull" : true, + "esnext" : false, + "evil" : false, + "expr" : false, + "funcscope" : false, + "globalstrict" : false, + "iterator" : false, + "lastsemic" : false, + "laxbreak" : false, + "laxcomma" : false, + "loopfunc" : false, + "multistr" : false, + "onecase" : false, + "proto" : false, + "regexdash" : false, + "scripturl" : false, + "smarttabs" : false, + "shadow" : false, + "sub" : false, + "supernew" : false, + "validthis" : false, + + "browser" : true, + "couch" : false, + "devel" : false, + "dojo" : false, + "jquery" : false, + "mootools" : false, + "node" : true, + "nonstandard" : false, + "prototypejs" : false, + "rhino" : false, + "wsh" : false, + + "nomen" : false, + "onevar" : false, + "passfail" : false, + "white" : false, + + "predef" : [ + "define" + ] } diff --git a/test/jasmine-test/spec/helpersTests.js b/test/jasmine-test/spec/helpersTests.js index fe0610e..0d9f670 100644 --- a/test/jasmine-test/spec/helpersTests.js +++ b/test/jasmine-test/spec/helpersTests.js @@ -1,3 +1,4 @@ +/*global dust*/ (function(dust) { var helpersTests = [ @@ -1562,6 +1563,6 @@ if (typeof exports !== "undefined") { module.exports = helpersTests; // We're on node.js } else { - this['helpersTests'] = helpersTests; // We're on the browser + this.helpersTests = helpersTests; // We're on the browser } })(typeof exports !== 'undefined' ? require('dustjs-linkedin') : dust); From 9ab15a2c647c2de75be574f3ebdab8d38504a053 Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 16:19:24 -0700 Subject: [PATCH 03/12] Remove deprecated helper `{@default}` and ensure `{@math}` works with `{@any}` and `{@none}`. --- lib/dust-helpers.js | 55 ++++++++++---------------- test/jasmine-test/spec/helpersTests.js | 29 ++++++++------ 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index ff9cb1a..18e7705 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -44,7 +44,6 @@ function addSelectState(context, key) { return newContext .push({ "__select__": { isResolved: false, - isDefaulted: false, isDeferredComplete: false, deferreds: [], key: key @@ -53,6 +52,16 @@ function addSelectState(context, key) { .push(head, context.stack.index, context.stack.of); } +function resolveSelectDeferreds(state) { + var x, len; + if(state.deferreds.length) { + state.isDeferredComplete = true; + for(x=0, len=state.deferreds.length; x{@math key="13" method="add" operand="12"}{@gt value=123}13 + 12 > 123{/gt}{@default}Math is fun{/default}{/math}', + source: '
{@math key="13" method="add" operand="12"}{@gt value=123}13 + 12 > 123{/gt}{@none}Math is fun{/none}{/math}
', context: {}, expected: "
Math is fun
", message: "testing math with body else helper with add and gt and default" @@ -395,12 +395,19 @@ message: "testing math with body ignores the else" }, { - name: "math helper with body acts like the select helper", + name: "math helper with body acts like the select helper, ignoring else", source: '
{@math key="1" method="subtract" operand="1"}math with body is truthy even if mathout is falsy{:else}else is meaningless{/math}
', context: {}, expected: "
math with body is truthy even if mathout is falsy
", message: "testing math with body ignores the else" }, + { + name: "math helper with body acts like the select helper and uses @any and @none", + source: '{@math key=1 method="subtract" operand=2}{@any}Positive!{/any}{@none}Negative!{/none}{@gte value=0/}{/math}', + context: {}, + expected: 'Negative!', + message: 'math helper with any and none' + }, { name: "math helper empty body", source: '
{@math key="1" method="add" operand="2"}{/math}
', @@ -897,7 +904,7 @@ "{@eq value=\"bar\"}foobar{/eq}", "{@eq value=\"baz\"}foobaz{/eq}", "{@eq value=\"foobar\"}foofoobar{/eq}", - "{@default value=\"foo\"}foofoo{/default}", + "{@none value=\"foo\"}foofoo{/none}", "{/select}" ].join("\n"), context: { "foo": "foo" }, @@ -944,7 +951,7 @@ source: ["{#b}{@select}", " {@eq value=\"{z}\"}
FOO
{/eq}", " {@eq value=\"{x}\"}
BAR
{/eq}", - " {@default}foofoo{/default}", + " {@none}foofoo{/none}", "{/select}{/b}"].join("\n"), context: { b : { z: "foo", x: "bar" } }, expected: "", @@ -956,7 +963,7 @@ source: ["{#b}{@select key=y}", " {@eq value=\"{z}\"}
FOO
{/eq}", " {@eq value=\"{x}\"}
BAR
{/eq}", - " {@default}foofoo{/default}", + " {@none}foofoo{/none}", "{/select}{/b}"].join("\n"), context: { b : { z: "foo", x: "bar" } }, expected: "foofoo", @@ -967,7 +974,7 @@ source: ["{#b}{@select key=\"{x}\"}", " {@eq value=\"{y}\"}
BAR
{/eq}", " {@eq value=\"{z}\"}
BAZ
{/eq}", - " {@default value=\"foo\"}foofoo{/default}", + " {@none value=\"foo\"}foofoo{/none}", "{/select}{/b}"].join("\n"), context: { b : { "x": "foo", "y": "bar", "z": "baz" } }, expected: "foofoo", @@ -978,7 +985,7 @@ source: ["{#skills}{@select key=.}", "{@eq value=\"java\"}JAVA,{/eq}", "{@eq value=\"js\"}JS,{/eq}", - "{@default value=\"foo\"}UNKNOWN{/default}", + "{@none value=\"foo\"}UNKNOWN{/none}", "{/select}{/skills}"].join("\n"), context: { "skills" : [ "java", "js" , "unknown"] }, expected: "JAVA,JS,UNKNOWN", @@ -989,7 +996,7 @@ source: ["{#skills}{@select key=\"{.}\"}", "{@eq value=\"java\"}JAVA,{/eq}", "{@eq value=\"js\"}JS,{/eq}", - "{@default value=\"foo\"}UNKNOWN{/default}", + "{@none value=\"foo\"}UNKNOWN{/none}", "{/select}{/skills}"].join("\n"), context: { "skills" : [ "java", "js" , "unknown"] }, expected: "JAVA,JS,UNKNOWN", @@ -1015,7 +1022,7 @@ source: ['{#skills}{@select key="{.}"}', '{@eq value="java"}JAVA {outside},{/eq}', '{@eq value="js"}JS {outside},{/eq}', - '{@default value="foo"}UNKNOWN {outside}{/default}', + '{@none value="foo"}UNKNOWN {outside}{/none}', '{/select}{/skills}'].join("\n"), context: { "skills" : [ "java", "js" , "unknown"], "outside": 'foo' }, expected: "JAVA foo,JS foo,UNKNOWN foo", @@ -1029,10 +1036,10 @@ ' done {message} ', ' {outside}', '{/eq} ', - '{@default}', + '{@none}', ' default {message} ', ' {outside}', - '{/default}', + '{/none}', '{/select}', '{/data.messages}'].join("\n"), context: { From d4f23ead7e97a457ea579d7d5ab16c4fcfa4c054 Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 16:45:14 -0700 Subject: [PATCH 04/12] Delegate `dust.helpers.tap` to native Dust `Context#resolve` --- lib/dust-helpers.js | 46 ++------------------------------------------- 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index 18e7705..77f5abb 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -143,53 +143,11 @@ function coerce(value, type, context) { 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 - - */ + // uses native Dust Context#resolve (available since Dust 2.6.2) "tap": function(input, chunk, context) { // deprecated for removal in 1.8 _deprecated("tap"); - - // 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; - } + return context.resolve(input); }, "sep": function(chunk, context, bodies) { From 55d1e60ccc054c00179ee8d6f92c9d201eb8a498 Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 16:56:07 -0700 Subject: [PATCH 05/12] Remove doc for `{@if}` Hi @jimmyhchan --- lib/dust-helpers.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index 77f5abb..87e3b60 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -207,21 +207,6 @@ var helpers = { 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 - **/ /** * math helper From e0fe45ad75d90798588f2f4b128c4872f458b8de Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 16:59:05 -0700 Subject: [PATCH 06/12] Simplify log. We know that dust.log will always be available due to the versioning policy --- lib/dust-helpers.js | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index 87e3b60..e1d072c 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -8,18 +8,17 @@ } }(this, function(dust) { -// Use dust's built-in logging when available -var _log = dust.log ? function(helper, msg, level) { +function log(helper, msg, level) { level = level || "INFO"; helper = helper ? '{@' + helper + '}: ' : ''; dust.log(helper + msg, level); -} : function() {}; +} var _deprecatedCache = {}; function _deprecated(target) { if(_deprecatedCache[target]) { return; } - _log(target, "Deprecation warning: " + target + " is deprecated and will be removed in a future version of dustjs-helpers", "WARN"); - _log(null, "For help and a deprecation timeline, see https://github.com/linkedin/dustjs-helpers/wiki/Deprecated-Features#" + target.replace(/\W+/g, ""), "WARN"); + log(target, "Deprecation warning: " + target + " is deprecated and will be removed in a future version of dustjs-helpers", "WARN"); + log(null, "For help and a deprecation timeline, see https://github.com/linkedin/dustjs-helpers/wiki/Deprecated-Features#" + target.replace(/\W+/g, ""), "WARN"); _deprecatedCache[target] = true; } @@ -102,7 +101,7 @@ function filter(chunk, context, bodies, params, filterOp) { filterOp = function() { return false; }; } } else { - _log(filterOpType, "No key specified", "WARN"); + log(filterOpType, "No key specified", "WARN"); return chunk; } expectedValue = dust.helpers.tap(params.value, chunk, context); @@ -197,7 +196,7 @@ var helpers = { dump = JSON.stringify(context.stack.head, jsonFilter, 2); } if (to === 'console') { - _log('contextDump', dump); + log('contextDump', dump); return chunk; } else { @@ -232,7 +231,7 @@ var helpers = { switch(method) { case "mod": if(operand === 0 || operand === -0) { - _log("math", "Division by 0", "ERROR"); + log("math", "Division by 0", "ERROR"); } mathOut = key % operand; break; @@ -247,7 +246,7 @@ var helpers = { break; case "divide": if(operand === 0 || operand === -0) { - _log("math", "Division by 0", "ERROR"); + log("math", "Division by 0", "ERROR"); } mathOut = key / operand; break; @@ -267,7 +266,7 @@ var helpers = { mathOut = parseInt(key, 10); break; default: - _log("math", "Method `" + method + "` is not supported", "ERROR"); + log("math", "Method `" + method + "` is not supported", "ERROR"); } if (mathOut !== null){ @@ -291,7 +290,7 @@ var helpers = { } // no key parameter and no method else { - _log("math", "`key` or `method` was not provided", "ERROR"); + log("math", "`key` or `method` was not provided", "ERROR"); } return chunk; }, @@ -315,10 +314,10 @@ var helpers = { chunk = chunk.render(body, context); resolveSelectDeferreds(getSelectState(context)); } else { - _log("select", "Missing body block", "WARN"); + log("select", "Missing body block", "WARN"); } } else { - _log("select", "`key` is required", "ERROR"); + log("select", "`key` is required", "ERROR"); } return chunk; }, @@ -429,10 +428,10 @@ var helpers = { var selectState = getSelectState(context); if(!selectState) { - _log("any", "Must be used inside a {@select} block", "ERROR"); + log("any", "Must be used inside a {@select} block", "ERROR"); } else { if(selectState.isDeferredComplete) { - _log("any", "Must not be nested inside {@any} or {@none} block", "ERROR"); + log("any", "Must not be nested inside {@any} or {@none} block", "ERROR"); } else { chunk = chunk.map(function(chunk) { selectState.deferreds.push(function() { @@ -457,10 +456,10 @@ var helpers = { var selectState = getSelectState(context); if(!selectState) { - _log("none", "Must be used inside a {@select} block", "ERROR"); + log("none", "Must be used inside a {@select} block", "ERROR"); } else { if(selectState.isDeferredComplete) { - _log("none", "Must not be nested inside {@any} or {@none} block", "ERROR"); + log("none", "Must not be nested inside {@any} or {@none} block", "ERROR"); } else { chunk = chunk.map(function(chunk) { selectState.deferreds.push(function() { From 3b2ddb09b0c572d3f9d3c25627627df839d8425a Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 17:30:16 -0700 Subject: [PATCH 07/12] Simplify `{@contextDump}` --- lib/dust-helpers.js | 46 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index e1d072c..d8b58cd 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -176,35 +176,31 @@ var helpers = { }, /** - * 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} + * @param key {String} set to "full" to the full context stack, otherwise the current context is dumped + * @param to {String} set to "console" to log to console, otherwise outputs to the chunk */ "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); + var to = context.resolve(params.to), + key = context.resolve(params.key), + target, output; + switch(key) { + case 'full': + target = context.stack; + break; + default: + target = context.stack.head; } - else { - dump = JSON.stringify(context.stack.head, jsonFilter, 2); - } - if (to === 'console') { - log('contextDump', dump); - return chunk; - } - else { - // encode opening brackets when outputting to html - dump = dump.replace(/ Date: Wed, 8 Apr 2015 17:57:23 -0700 Subject: [PATCH 08/12] Simplify filter Short-circuiting had an incorrect behavior where `{:else}` blocks would be invoked. Remove `type="context"`; you should just pass the key directly and there was no test for it anyways Don't expose `filterOpType` as a param; you might clobber a real param --- lib/dust-helpers.js | 53 +++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index d8b58cd..8aaebee 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -79,50 +79,48 @@ function jsonFilter(key, value) { return value; } -// Utility method: to invoke the given filter operation such as eq/gt etc -function filter(chunk, context, bodies, params, filterOp) { - params = params || {}; +// This function is invoked by equality helpers +function filter(chunk, context, bodies, params, helperName, test) { var body = bodies.block, - actualKey, - expectedValue, + skip = bodies.else, selectState = getSelectState(context), - filterOpType = params.filterOpType || ''; + key, value; // Currently we first check for a key on the helper itself, then fall back to // looking for a key on the {@select} that contains it. This is undocumented // behavior that we may or may not support in the future. (If we stop supporting // it, just switch the order of the test below to check the {@select} first.) if (params.hasOwnProperty("key")) { - actualKey = dust.helpers.tap(params.key, chunk, context); + key = context.resolve(params.key); } else if (selectState) { - actualKey = selectState.key; + key = selectState.key; // Once one truth test in a select passes, short-circuit the rest of the tests if (selectState.isResolved) { - filterOp = function() { return false; }; + return chunk; } } else { - log(filterOpType, "No key specified", "WARN"); + log(helperName, "No key specified", "WARN"); 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))) { + + key = coerce(key, params.type); + value = coerce(context.resolve(params.value), params.type); + + if (test(key, value)) { if (selectState) { selectState.isResolved = true; } // Helpers without bodies are valid due to the use of {@any} blocks if(body) { return chunk.render(body, context); - } else { - return chunk; } - } else if (bodies['else']) { - return chunk.render(bodies['else'], context); + } else if (skip) { + return chunk.render(skip, context); } return chunk; } -function coerce(value, type, context) { +function coerce(value, type) { if (typeof value !== 'undefined' && typeof type !== 'undefined') { switch (type) { @@ -132,7 +130,6 @@ function coerce(value, type, context) { value = (value === 'false' ? false : value); return Boolean(value); case 'date': return new Date(value); - case 'context': return context.get(value); } } @@ -330,8 +327,7 @@ var helpers = { Note : use type="number" when comparing numeric **/ "eq": function(chunk, context, bodies, params) { - params.filterOpType = "eq"; - return filter(chunk, context, bodies, params, function(expected, actual) { return actual === expected; }); + return filter(chunk, context, bodies, params, 'eq', function(left, right) { return left === right; }); }, /** @@ -346,8 +342,7 @@ var helpers = { Note : use type="number" when comparing numeric **/ "ne": function(chunk, context, bodies, params) { - params.filterOpType = "ne"; - return filter(chunk, context, bodies, params, function(expected, actual) { return actual !== expected; }); + return filter(chunk, context, bodies, params, 'ne', function(left, right) { return left !== right; }); }, /** @@ -362,8 +357,7 @@ var helpers = { Note : use type="number" when comparing numeric **/ "lt": function(chunk, context, bodies, params) { - params.filterOpType = "lt"; - return filter(chunk, context, bodies, params, function(expected, actual) { return actual < expected; }); + return filter(chunk, context, bodies, params, 'lt', function(left, right) { return left < right; }); }, /** @@ -378,8 +372,7 @@ var helpers = { Note : use type="number" when comparing numeric **/ "lte": function(chunk, context, bodies, params) { - params.filterOpType = "lte"; - return filter(chunk, context, bodies, params, function(expected, actual) { return actual <= expected; }); + return filter(chunk, context, bodies, params, 'lte', function(left, right) { return left <= right; }); }, /** @@ -394,8 +387,7 @@ var helpers = { Note : use type="number" when comparing numeric **/ "gt": function(chunk, context, bodies, params) { - params.filterOpType = "gt"; - return filter(chunk, context, bodies, params, function(expected, actual) { return actual > expected; }); + return filter(chunk, context, bodies, params, 'gt', function(left, right) { return left > right; }); }, /** @@ -410,8 +402,7 @@ var helpers = { Note : use type="number" when comparing numeric **/ "gte": function(chunk, context, bodies, params) { - params.filterOpType = "gte"; - return filter(chunk, context, bodies, params, function(expected, actual) { return actual >= expected; }); + return filter(chunk, context, bodies, params, 'gte', function(left, right) { return left >= right; }); }, /** From 59850f586e034b564e91a58aea67a77349f418ae Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 18:20:21 -0700 Subject: [PATCH 09/12] Simplify `{@math}` Like other helpers, allow `key` to be undefined as long as it was provided. --- lib/dust-helpers.js | 143 ++++++++++++++++++++------------------------ 1 file changed, 64 insertions(+), 79 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index 8aaebee..de385e1 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -201,90 +201,75 @@ var helpers = { }, /** - * 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} + * @param key first value + * @param method {String} operation to perform + * @param operand second value (not required for operations like `abs` or ``) + * @param round if truthy, round() the result */ - "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, - state, x, len; - - key = parseFloat(dust.helpers.tap(key, chunk, context)); - operand = parseFloat(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("math", "Division by 0", "ERROR"); - } - mathOut = key % operand; - break; - case "add": - mathOut = key + operand; - break; - case "subtract": - mathOut = key - operand; - break; - case "multiply": - mathOut = key * operand; - break; - case "divide": - if(operand === 0 || operand === -0) { - log("math", "Division by 0", "ERROR"); - } - mathOut = key / operand; - break; - case "ceil": - mathOut = Math.ceil(key); - break; - case "floor": - mathOut = Math.floor(key); - break; - case "round": - mathOut = Math.round(key); - break; - case "abs": - mathOut = Math.abs(key); - break; - case "toint": - mathOut = parseInt(key, 10); - break; - default: - log("math", "Method `" + method + "` is not supported", "ERROR"); - } - - if (mathOut !== null){ - if (round) { - mathOut = Math.round(mathOut); + "math": function (chunk, context, bodies, params) { + var key = params.key, + method = params.method, + operand = params.operand, + round = params.round, + output, state, x, len; + + if(!params.hasOwnProperty('key') || !params.method) { + log("math", "`key` or `method` was not provided", "ERROR"); + return chunk; + } + + key = parseFloat(context.resolve(key)); + operand = parseFloat(context.resolve(operand)); + + switch(method) { + case "mod": + if(operand === 0) { + log("math", "Division by 0", "ERROR"); } - 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 - context = addSelectState(context, mathOut); - chunk = chunk.render(bodies.block, context); - resolveSelectDeferreds(getSelectState(context)); - return chunk; - } else { - // self closing math helper will return the calculated output - return chunk.write(mathOut); + output = key % operand; + break; + case "add": + output = key + operand; + break; + case "subtract": + output = key - operand; + break; + case "multiply": + output = key * operand; + break; + case "divide": + if(operand === 0) { + log("math", "Division by 0", "ERROR"); } - } else { - return chunk; - } + output = key / operand; + break; + case "ceil": + case "floor": + case "round": + case "abs": + output = Math[method](key); + break; + case "toint": + output = parseInt(key, 10); + break; + default: + log("math", "Method `" + method + "` is not supported", "ERROR"); } - // no key parameter and no method - else { - log("math", "`key` or `method` was not provided", "ERROR"); + + if (typeof output !== 'undefined') { + if (round) { + output = Math.round(output); + } + if (bodies && bodies.block) { + context = addSelectState(context, output); + chunk = chunk.render(bodies.block, context); + resolveSelectDeferreds(getSelectState(context)); + } else { + chunk = chunk.write(output); + } } + return chunk; }, /** From 2ec436f3cf7db7221f7135fbeace140f09e9ce62 Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 18:36:00 -0700 Subject: [PATCH 10/12] Generate truth tests to cut down boilerplate --- lib/dust-helpers.js | 169 +++++++++++++++----------------------------- 1 file changed, 57 insertions(+), 112 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index de385e1..c369387 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -32,6 +32,10 @@ function getSelectState(context) { return isSelect(context) && context.get('__select__'); } +/** + * Adds a special __select__ key behind the head of the context stack. Used to maintain the state + * of {@select} blocks + */ function addSelectState(context, key) { var head = context.stack.head, newContext = context.rebase(); @@ -51,6 +55,9 @@ function addSelectState(context, key) { .push(head, context.stack.index, context.stack.of); } +/** + * After a {@select} or {@math} block is complete, they invoke this function + */ function resolveSelectDeferreds(state) { var x, len; if(state.deferreds.length) { @@ -61,25 +68,32 @@ function resolveSelectDeferreds(state) { } } -// Utility method : toString() equivalent for functions +/** + * Used by {@contextDump} + */ 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, ') {') - ; + .replace(/\)\{/mg, ') {'); } return value; } -// This function is invoked by equality helpers +/** + * Generate a truth test helper + */ +function truthTest(name, test) { + return function(chunk, context, bodies, params) { + return filter(chunk, context, bodies, params, name, test); + }; +} + +/** + * This function is invoked by truth test helpers + */ function filter(chunk, context, bodies, params, helperName, test) { var body = bodies.block, skip = bodies.else, @@ -110,7 +124,6 @@ function filter(chunk, context, bodies, params, helperName, test) { if (selectState) { selectState.isResolved = true; } - // Helpers without bodies are valid due to the use of {@any} blocks if(body) { return chunk.render(body, context); } @@ -272,21 +285,19 @@ var helpers = { 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} + * Groups a set of truth tests and outputs the first one that passes. + * Also contains {@any} and {@none} blocks. + * @param key a value or reference to use as the left-hand side of comparisons + */ "select": function(chunk, context, bodies, params) { var body = bodies.block, key; if (params.hasOwnProperty("key")) { key = dust.helpers.tap(params.key, chunk, context); - // bodies['else'] is meaningless and is ignored if (body) { context = addSelectState(context, key); chunk = chunk.render(body, context); @@ -301,94 +312,29 @@ var helpers = { }, /** - 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) { - return filter(chunk, context, bodies, params, 'eq', function(left, right) { return left === right; }); - }, - - /** - 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) { - return filter(chunk, context, bodies, params, 'ne', function(left, right) { return left !== right; }); - }, - - /** - 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) { - return filter(chunk, context, bodies, params, 'lt', function(left, right) { return left < right; }); - }, - - /** - 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) { - return filter(chunk, context, bodies, params, 'lte', function(left, right) { return left <= right; }); - }, - - /** - 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) { - return filter(chunk, context, bodies, params, 'gt', function(left, right) { return left > right; }); - }, - - /** - 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) { - return filter(chunk, context, bodies, params, 'gte', function(left, right) { return left >= right; }); - }, + * Truth test helpers + * @param key a value or reference to use as the left-hand side of comparisons + * @param value a value or reference to use as the left-hand side of comparisons + * @param type if specified, `key` and `value` will be forcibly cast to this type + */ + "eq": truthTest('eq', function(left, right) { + return left === right; + }), + "ne": truthTest('ne', function(left, right) { + return left !== right; + }), + "lt": truthTest('lt', function(left, right) { + return left < right; + }), + "lte": truthTest('lte', function(left, right) { + return left <= right; + }), + "gt": truthTest('gt', function(left, right) { + return left > right; + }), + "gte": truthTest('gte', function(left, right) { + return left >= right; + }), /** * {@any} @@ -480,13 +426,12 @@ var helpers = { return chunk.write(value); } - }; - for(var key in helpers) { - dust.helpers[key] = helpers[key]; - } +for(var key in helpers) { + dust.helpers[key] = helpers[key]; +} - return dust; +return dust; })); From def2b567e715d40a1917c9b6c97b291248a0cfd4 Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 18:50:00 -0700 Subject: [PATCH 11/12] {@size} resolves Dust bodies and outputs their size Closes linkedin/dustjs-helpers#46 --- lib/dust-helpers.js | 43 ++++++++++---------- test/jasmine-test/spec/helpersTests.js | 56 +++++++++++++------------- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index c369387..a0ff384 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -393,35 +393,36 @@ var helpers = { }, /** - * 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} + * Write the size of the target to the chunk + * Falsy values and true have size 0 + * Numbers are returned as-is + * Arrays and Strings have size equal to their length + * Objects have size equal to the number of keys they contain + * Dust bodies are evaluated and the length of the string is returned + * Functions are evaluated and the length of their return value is evaluated + * @param key find the size of this value or reference */ - "size": function( chunk, context, bodies, params ) { - var key, value=0, nr, k; - params = params || {}; - key = params.key; - if (!key || key === true) { //undefined, null, "", 0 + "size": function(chunk, context, bodies, params) { + var key = params.key, + value, k; + + key = context.resolve(params.key); + if (!key || key === true) { value = 0; - } - else if(dust.isArray(key)) { //array + } else if(dust.isArray(key)) { value = key.length; - } - else if (!isNaN(parseFloat(key)) && isFinite(key)) { //numeric values + } else if (!isNaN(parseFloat(key)) && isFinite(key)) { 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; + } else if (typeof key === "object") { + value = 0; for(k in key){ - if(Object.hasOwnProperty.call(key,k)){ - nr++; + if(key.hasOwnProperty(k)){ + value++; } } - value = nr; } else { - value = (key + '').length; //any other value (strings etc.) + value = (key + '').length; } return chunk.write(value); } diff --git a/test/jasmine-test/spec/helpersTests.js b/test/jasmine-test/spec/helpersTests.js index c3b53a2..a2c64b0 100644 --- a/test/jasmine-test/spec/helpersTests.js +++ b/test/jasmine-test/spec/helpersTests.js @@ -1209,105 +1209,105 @@ source: 'you have {@size key=list}{body}{/size} new messages', context: { list: [ 'msg1', 'msg2', 'msg3' ], "body" : "body block" }, expected: "you have 3 new messages", - message: "should test size helper not supporting body" + message: "should test {@size} skips its body" }, { name: "size helper 3 items", source: 'you have {@size key=list/} new messages', context: { list: [ 'msg1', 'msg2', 'msg3' ] }, expected: "you have 3 new messages", - message: "should test if size helper is working properly with array" + message: "should test {@size} with an array" }, { name: "size helper string", source: "'{mystring}' has {@size key=mystring/} letters", context: { mystring: 'hello' }, expected: "'hello' has 5 letters", - message: "should test if size helper is working properly with strings" + message: "should test {@size} with a string" }, { name: "size helper string (empty)", source: "'{mystring}' has {@size key=mystring/} letters", context: { mystring: '' }, expected: "'' has 0 letters", - message: "should test if size helper is working properly with strings" + message: "should test {@size} with an empty string" }, { - name: "size helper for newline", - source: "{@size key=mystring/} letters", - context: { mystring: '\n' }, - expected: "1 letters", - message: "should test if size is working for newline" - }, - { - name: "size helper string with newline", - source: "{@size key=mystring/} letters", - context: { mystring: 'test\n' }, - expected: "5 letters", - message: "should test if size for string with newline" - }, - { - name: "size helper string with newline, tab, carriage return and bakspace", + name: "size helper string with newline, tab, carriage return and backspace", source: "{@size key=mystring/} letters", context: { mystring: 'test\n\t\r\b' }, expected: "8 letters", - message: "should test if size helper is working for string with newline, tab, carriage return and bakspace" + message: "should test {@size} with character literals in a string" }, { name: "size helper number", source: 'you have {@size key=mynumber/} new messages', context: { mynumber: 0 }, expected: "you have 0 new messages", - message: "should test if size helper is working properly with numeric 0" + message: "should test {@size} with 0" }, { name: "size helper number", source: 'you have {@size key=mynumber/} new messages', context: { mynumber: 10 }, expected: "you have 10 new messages", - message: "should test if size helper is working properly with numeric 10" + message: "should test {@size} with an int" }, { name: "size helper floating numeric", source: 'you have {@size key=mynumber/} new messages', context: { mynumber: 0.4 }, expected: "you have 0.4 new messages", - message: "should test if size helper is working properly with floating numeric" + message: "should test {@size} with a float" }, { name: "size helper with boolean false", source: 'you have {@size key=myboolean/} new messages', context: { myboolean: false }, expected: "you have 0 new messages", - message: "should test if size helper is working properly with boolean false" + message: "should test {@size} with false" }, { name: "size helper with boolean true", source: 'you have {@size key=myboolean/} new messages', context: { myboolean: true }, expected: "you have 0 new messages", - message: "should test if size helper is working properly with boolean true" + message: "should test {@size} with true" }, { name: "size helper with object", source: 'you have {@size key=myValue/} new messages', context: { myValue: { foo:'bar', baz:'bax' } }, expected: "you have 2 new messages", - message: "should test if size helper is working properly when the value is an object " + message: "should test {@size} with an Object" }, { name: "size helper with object", source: 'you have {@size key=myValue/} new messages', context: { myValue: {} }, expected: "you have 0 new messages", - message: "should test if size helper is working properly when the value is an object that is zero" + message: "should test {@size} with an empty Object" }, { name: "size helper value not set", source: 'you have {@size key=myNumber/} new messages', context: {}, expected: "you have 0 new messages", - message: "should test if size helper is working properly when the value is not present in context" + message: "should test {@size} with an undefined value" + }, + { + name: "size helper function", + source: 'you have {@size key=func/} new messages', + context: { func: function() { return 4; } }, + expected: "you have 4 new messages", + message: "should test {@size} with a function" + }, + { + name: "size body function", + source: '"hello" has {@size key="{func}"/} letters', + context: { func: function() { return 'hello'; } }, + expected: '"hello" has 5 letters', + message: "should test {@size} with a Dust body" } ] }, From 43dcf6eddaf6d3afeb90978ad2dbc369dc9f4583 Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 8 Apr 2015 18:54:43 -0700 Subject: [PATCH 12/12] Simplify `{@select}` --- lib/dust-helpers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index a0ff384..3f0d1f0 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -217,7 +217,7 @@ var helpers = { * {@math} * @param key first value * @param method {String} operation to perform - * @param operand second value (not required for operations like `abs` or ``) + * @param operand second value (not required for operations like `abs`) * @param round if truthy, round() the result */ "math": function (chunk, context, bodies, params) { @@ -297,7 +297,7 @@ var helpers = { key; if (params.hasOwnProperty("key")) { - key = dust.helpers.tap(params.key, chunk, context); + key = context.resolve(params.key); if (body) { context = addSelectState(context, key); chunk = chunk.render(body, context); @@ -314,7 +314,7 @@ var helpers = { /** * Truth test helpers * @param key a value or reference to use as the left-hand side of comparisons - * @param value a value or reference to use as the left-hand side of comparisons + * @param value a value or reference to use as the right-hand side of comparisons * @param type if specified, `key` and `value` will be forcibly cast to this type */ "eq": truthTest('eq', function(left, right) {