Skip to content

Commit

Permalink
Merge pull request #107 from sethkinast/select-improvements
Browse files Browse the repository at this point in the history
Add {@any} helper for use within an {@select} body.
  • Loading branch information
prashn64 committed Dec 22, 2014
2 parents 3584658 + f96f820 commit eb10839
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 66 deletions.
176 changes: 111 additions & 65 deletions lib/dust-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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']) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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<len; x++) {
state.deferreds[x]();
}
}
} else {
_log("Missing body block in {@select}");
}
}
// no key
else {
_log("No key given in the select helper!");
} else {
_log("No key provided for {@select}");
}
return chunk;
},
Expand All @@ -369,11 +402,8 @@ var helpers = {
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; });
}
return chunk;
params.filterOpType = "eq";
return filter(chunk, context, bodies, params, function(expected, actual) { return actual === expected; });
},

/**
Expand All @@ -388,11 +418,8 @@ var helpers = {
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;
params.filterOpType = "ne";
return filter(chunk, context, bodies, params, function(expected, actual) { return actual !== expected; });
},

/**
Expand All @@ -407,11 +434,8 @@ var helpers = {
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; });
}
return chunk;
params.filterOpType = "lt";
return filter(chunk, context, bodies, params, function(expected, actual) { return actual < expected; });
},

/**
Expand All @@ -426,14 +450,10 @@ var helpers = {
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;
params.filterOpType = "lte";
return filter(chunk, context, bodies, params, function(expected, actual) { return actual <= expected; });
},


/**
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
Expand All @@ -446,12 +466,8 @@ var helpers = {
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;
params.filterOpType = "gt";
return filter(chunk, context, bodies, params, function(expected, actual) { return actual > expected; });
},

/**
Expand All @@ -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; });
},

/**
Expand Down
104 changes: 104 additions & 0 deletions test/jasmine-test/spec/helpersTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
},
Expand Down
Loading

0 comments on commit eb10839

Please sign in to comment.