From f96f820621870a31fcd6063b46a989021dea1d1b Mon Sep 17 00:00:00 2001 From: Seth Kinast Date: Wed, 10 Dec 2014 23:18:56 -0800 Subject: [PATCH] Add {@any} helper. - Must be contained in a {@select} block - Renders its body if any truth test inside the {@select} block evaluates to true - Can be included multiple times inside a {@select} block. As part of this change, {@select} no longer clobbers the current context, so keys that begin with a dot work inside a {@select}. This closes #9, closes #42, closes #50, and closes #79. --- lib/dust-helpers.js | 176 ++++++++++++++--------- test/jasmine-test/spec/helpersTests.js | 104 ++++++++++++++ test/jasmine-test/spec/renderTestSpec.js | 4 +- 3 files changed, 218 insertions(+), 66 deletions(-) diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index ccc105a..c2d52c8 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -15,8 +15,27 @@ function _deprecated(target) { } function isSelect(context) { - var value = context.current(); - return typeof value === "object" && value.isSelect === true; + return context.stack.tail && + typeof context.stack.tail.head.__select__ !== "undefined"; +} + +function getSelectState(context) { + return context.get('__select__'); +} + +function addSelectState(context, key) { + var head = context.stack.head; + return context + .rebase(context.stack.tail) + .push({ "__select__": { + isResolved: false, + isDefaulted: false, + isDeferredComplete: false, + deferreds: [], + key: key + } + }) + .push(head); } // Utility method : toString() equivalent for functions @@ -43,32 +62,39 @@ function filter(chunk, context, bodies, params, filterOp) { var body = bodies.block, actualKey, expectedValue, + selectState, filterOpType = params.filterOpType || ''; - // when @eq, @lt etc are used as standalone helpers, key is required and hence check for defined + // 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); } else if (isSelect(context)) { - actualKey = context.current().selectKey; - // supports only one of the blocks in the select to be selected - if (context.current().isResolved) { + selectState = getSelectState(context); + actualKey = selectState.key; + // Once one truth test in a select passes, short-circuit the rest of the tests + if (selectState.isResolved) { filterOp = function() { return false; }; } } else { - _log("No key specified for filter in:" + filterOpType + " helper "); + _log("No key specified for filter in {@" + filterOpType + "}"); 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; + if(filterOpType === 'default') { + selectState.isDefaulted = true; + } + selectState.isResolved = true; } - // we want helpers without bodies to fail gracefully so check it first + // Helpers without bodies are valid due to the use of {@any} blocks if(body) { - return chunk.render(body, context); + return chunk.render(body, context); } else { - _log("No body specified for " + filterOpType + " helper "); return chunk; } } else if (bodies['else']) { @@ -264,6 +290,7 @@ var helpers = { _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 @@ -312,7 +339,8 @@ var helpers = { 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 })); + context = addSelectState(context, mathOut); + return chunk.render(bodies.block, context); } else { // self closing math helper will return the calculated output return chunk.write(mathOut); @@ -336,23 +364,28 @@ var helpers = { @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); + var body = bodies.block, + state, key, len, x; + + if (params && typeof params.key !== "undefined") { + 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; + if (body) { + context = addSelectState(context, key); + state = getSelectState(context); + chunk = chunk.render(body, context); + // Resolve any deferred blocks (currently just {@any} blocks) + if(state.deferreds.length) { + state.isDeferredComplete = true; + for(x=0, len=state.deferreds.length; x expected; }); - } - return chunk; + params.filterOpType = "gt"; + return filter(chunk, context, bodies, params, function(expected, actual) { return actual > expected; }); }, /** @@ -466,21 +482,51 @@ var helpers = { 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; }); - } + params.filterOpType = "gte"; + return filter(chunk, context, bodies, params, function(expected, actual) { return actual >= expected; }); + }, + + /** + * {@any} + * Outputs as long as at least one truth test inside a {@select} has passed. + * Must be contained inside a {@select} block. + * The passing truth test can be before or after the {@any} block. + */ + "any": function(chunk, context, bodies, params) { + var selectState; + + if(!isSelect(context)) { + _log("{@any} used outside of a {@select} block", "WARN"); + } else { + selectState = getSelectState(context); + if(selectState.isDeferredComplete) { + _log("{@any} nested inside {@any} block. It needs its own {@select} block", "WARN"); + } else { + chunk = chunk.map(function(chunk) { + selectState.deferreds.push(function() { + if(selectState.isResolved && !selectState.isDefaulted) { + chunk = chunk.render(bodies.block, context); + } + chunk.end(); + }); + }); + } + } return chunk; }, - // to be used in conjunction with the select helper - // TODO: fix the helper to do nothing when used standalone + /** + * {@default} + * Outputs if no truth test inside a {@select} has passed. + * Must be contained inside a {@select} block. + */ "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; }); + params.filterOpType = "default"; + if(!isSelect(context)) { + _log("{@default} used outside of a {@select} block", "WARN"); + return chunk; + } + return filter(chunk, context, bodies, params, function() { return true; }); }, /** diff --git a/test/jasmine-test/spec/helpersTests.js b/test/jasmine-test/spec/helpersTests.js index 89e75d9..877d2a6 100644 --- a/test/jasmine-test/spec/helpersTests.js +++ b/test/jasmine-test/spec/helpersTests.js @@ -1016,6 +1016,110 @@ context: { "skills" : [ "java", "js" , "unknown"] }, expected: "JAVA,JS,UNKNOWN", message: "should test select helper inside a array with {.}" + }, + { + name: "select helper doesn't destroy current context", + source: '{#test}{@select key=foo}{@eq value="{.bar_ref}"}{.name}{/eq}{/select}{/test}', + context: { + "name": "Wrong", + "bar_ref": "Wrong", + "test": { + "foo": "bar", + "bar_ref": "bar", + "name": "Right" + } + }, + expected: "Right", + message: "should test that the current context is still accessible within the select" + } + ] + }, + { + name: "any", + tests: [ + { + name: "any without select", + source: '{@any}Hello{/any}', + context: { any: 'abc'}, + expected: "", + message: "any helper outside of select does not render" + }, + { + name: "any in select with no cases", + source: '{@select key=foo}{@any}Hello{/any}{/select}', + context: { foo: "bar"}, + expected: "", + message: "any helper with no cases in the select does not render" + }, + { + name: "any in select with no true cases", + source: '{@select key=foo}{@eq value=1/}{@any}Hello{/any}{/select}', + context: { foo: "bar"}, + expected: "", + message: "any helper with no true cases in the select does not render" + }, + { + name: "any in select with one true case", + source: '{@select key=foo}{@eq value="bar"/}{@any}Hello{/any}{/select}', + context: { foo: "bar"}, + expected: "Hello", + message: "any helper with a true case in the select renders" + }, + { + name: "any in select with multiple cases, one true", + source: '{@select key=foo}{@eq value="no"/}{@eq value="bar"/}{@any}Hello{/any}{/select}', + context: { foo: "bar"}, + expected: "Hello", + message: "any helper with at least one true case in the select renders" + }, + { + name: "any in select with true case that has a body", + source: '{@select key=foo}{@eq value="bar"}World {/eq}{@any}Hello{/any}{/select}', + context: { foo: "bar"}, + expected: "World Hello", + message: "any helper in select renders along with the true case's body" + }, + { + name: "any in select that comes before the true case", + source: '{@select key=foo}{@any}Hello{/any}{@eq value="bar"} World{/eq}{/select}', + context: { foo: "bar"}, + expected: "Hello World", + message: "any helper that comes before the true case still renders" + }, + { + name: "multiple any helpers", + source: '{@select key=foo}{@any}Hello{/any}{@eq value="bar"/}{@any} World{/any}{/select}', + context: { foo: "bar"}, + expected: "Hello World", + message: "multiple any helpers in the same select all render" + }, + { + name: "multiple nested any helpers, false case", + source: '{@select key=foo}{@any}Hello{/any}{@eq value="bar"}{@select key=moo}{@eq value="cow"/}{@any} Cow{/any}{/select}{/eq}{@any} World{/any}{/select}', + context: { foo: "bar", moo: "shoo"}, + expected: "Hello World", + message: "multiple any helpers in nested selects work correctly if their select has no true test" + }, + { + name: "multiple nested any helpers, true case", + source: '{@select key=foo}{@any}Hello{/any}{@eq value="bar"}{@select key=moo}{@eq value="cow"/}{@any} Cow{/any}{/select}{/eq}{@any} World{/any}{/select}', + context: { foo: "bar", moo: "cow"}, + expected: "Hello Cow World", + message: "multiple any helpers in nested selects render if each select has a true test" + }, + { + name: "any nested in an any, it's anyception", + source: '{@select key=foo}{@eq value="bar"/}{@any}Hello{@any} World{/any}{/any}{/select}', + context: { foo: "bar"}, + expected: "Hello", + message: "an any helper cannot be nested inside an any helper without a select" + }, + { + name: "any nested in an any properly with its own select", + source: '{@select key=foo}{@eq value="bar"/}{@any}Hello{@select key=moo}{@eq value="cow"/}{@any} World{/any}{/select}{/any}{/select}', + context: { foo: "bar", moo: "cow"}, + expected: "Hello World", + message: "an any helper must have its own select to render" } ] }, diff --git a/test/jasmine-test/spec/renderTestSpec.js b/test/jasmine-test/spec/renderTestSpec.js index cbf0acb..3a24b59 100644 --- a/test/jasmine-test/spec/renderTestSpec.js +++ b/test/jasmine-test/spec/renderTestSpec.js @@ -10,6 +10,8 @@ } }(this, function(dust, helpersTests) { + dust.debugLevel = "DEBUG"; + function render(test) { return function() { try { @@ -113,4 +115,4 @@ } } }); -})); \ No newline at end of file +}));