diff --git a/.jshintrc b/.jshintrc index c862aec..e66f0ed 100644 --- a/.jshintrc +++ b/.jshintrc @@ -16,7 +16,8 @@ "dust": true, "require": true, "module": true, + "define": true, "console": true, "__dirname": true } -} \ No newline at end of file +} diff --git a/lib/dust-helpers.js b/lib/dust-helpers.js index e7ecdc3..d8b033a 100644 --- a/lib/dust-helpers.js +++ b/lib/dust-helpers.js @@ -1,4 +1,12 @@ -(function(dust){ +(function(root, factory) { + if (typeof define === 'function' && define.amd && define.amd.dust === true) { + define(['dust.core'], factory); + } else if (typeof exports === 'object') { + module.exports = factory(require('dustjs-linkedin')); + } else { + factory(root.dust); + } +}(this, function(dust) { // Use dust's built-in logging when available var _log = dust.log ? function(msg, level) { @@ -180,6 +188,20 @@ var helpers = { } }, + "first": function(chunk, context, bodies) { + if (context.stack.index === 0) { + return bodies.block(chunk, context); + } + return chunk; + }, + + "last": function(chunk, context, bodies) { + if (context.stack.index === context.stack.of - 1) { + return bodies.block(chunk, context); + } + return chunk; + }, + /** * contextDump helper * @param key specifies how much to dump. @@ -324,7 +346,7 @@ var helpers = { var body = bodies.block, state, key, len, x; - if (params && typeof params.key !== "undefined") { + if (params.hasOwnProperty("key")) { key = dust.helpers.tap(params.key, chunk, context); // bodies['else'] is meaningless and is ignored if (body) { @@ -457,7 +479,7 @@ var helpers = { } else { selectState = getSelectState(context); if(selectState.isDeferredComplete) { - _log("{@any} nested inside {@any} block. It needs its own {@select} block", "WARN"); + _log("{@any} nested inside {@any} or {@none} block. It needs its own {@select} block", "WARN"); } else { chunk = chunk.map(function(chunk) { selectState.deferreds.push(function() { @@ -472,6 +494,35 @@ var helpers = { return chunk; }, + /** + * {@none} + * Outputs if no truth tests inside a {@select} pass. + * Must be contained inside a {@select} block. + * The position of the helper does not matter. + */ + "none": function(chunk, context, bodies, params) { + var selectState; + + if(!isSelect(context)) { + _log("{@none} used outside of a {@select} block", "WARN"); + } else { + selectState = getSelectState(context); + if(selectState.isDeferredComplete) { + _log("{@none} nested inside {@any} or {@none} block. It needs its own {@select} block", "WARN"); + } else { + chunk = chunk.map(function(chunk) { + selectState.deferreds.push(function() { + if(!selectState.isResolved) { + chunk = chunk.render(bodies.block, context); + } + chunk.end(); + }); + }); + } + } + return chunk; + }, + /** * {@default} * Outputs if no truth test inside a {@select} has passed. @@ -479,6 +530,8 @@ var helpers = { */ "default": function(chunk, context, bodies, params) { params.filterOpType = "default"; + // Deprecated for removal in 1.7 + _deprecated("{@default}"); if(!isSelect(context)) { _log("{@default} used outside of a {@select} block", "WARN"); return chunk; @@ -523,12 +576,10 @@ var helpers = { }; - for (var key in helpers) { + for(var key in helpers) { dust.helpers[key] = helpers[key]; } - if(typeof exports !== 'undefined') { - module.exports = dust; - } + return dust; -})(typeof exports !== 'undefined' ? require('dustjs-linkedin') : dust); +})); diff --git a/test/jasmine-test/spec/helpersTests.js b/test/jasmine-test/spec/helpersTests.js index 3ef94f7..a307cdc 100644 --- a/test/jasmine-test/spec/helpersTests.js +++ b/test/jasmine-test/spec/helpersTests.js @@ -918,10 +918,9 @@ expected: "foobar", message: "should test select helper with variable and type string in a nested objects" }, - { - name: "select helper with missing key in the context and hence no output", - source: ["{#b}{@select key=y}", + name: "select helper with missing key parameter and hence no output", + source: ["{#b}{@select}", " {@eq value=\"{z}\"}
FOO
{/eq}", " {@eq value=\"{x}\"}
BAR
{/eq}", " {@default}foofoo{/default}", @@ -930,6 +929,17 @@ expected: "", message: "should test select helper with missing key in the context and hence no output" }, + { + name: "select helper with key not defined in the context", + source: ["{#b}{@select key=y}", + " {@eq value=\"{z}\"}
FOO
{/eq}", + " {@eq value=\"{x}\"}
BAR
{/eq}", + " {@default}foofoo{/default}", + "{/select}{/b}"].join("\n"), + context: { b : { z: "foo", x: "bar" } }, + expected: "foofoo", + message: "should test select helper with undefined key in the context" + }, { name: "select helper wih key matching the default condition", source: ["{#b}{@select key=\"{x}\"}", @@ -1069,6 +1079,54 @@ } ] }, + { + name: "none", + tests: [ + { + name: "none without select", + source: '{@none}Hello{/none}', + context: { none: 'abc'}, + expected: "", + message: "none helper outside of select does not render" + }, + { + name: "none in select with no cases", + source: '{@select key=foo}{@none}Hello{/none}{/select}', + context: { foo: "bar"}, + expected: "Hello", + message: "none helper with no cases in the select renders" + }, + { + name: "none in select with no true cases", + source: '{@select key=foo}{@eq value=1/}{@none}Hello{/none}{/select}', + context: { foo: "bar"}, + expected: "Hello", + message: "none helper with no true cases in the select renders" + }, + { + name: "none in select with one true case", + source: '{@select key=foo}{@eq value="bar"/}{@none}Hello{/none}{/select}', + context: { foo: "bar"}, + expected: "", + message: "none helper with a true case in the select does not render" + }, + { + name: "multiple none helpers", + source: '{@select key=foo}{@none}Hello{/none}{@eq value="cow"/}{@none} World{/none}{/select}', + context: { foo: "bar"}, + expected: "Hello World", + message: "multiple none helpers in the same select all render" + }, + { + name: "none nested in an none properly with its own select", + source: '{@select key=foo}{@eq value="bar"/}{@none}Hello{@select key=moo}{@eq value="cow"/}{@none} World{/none}{/select}{/none}{/select}', + context: { foo: true, moo: true}, + expected: "Hello World", + message: "a none helper must have its own select to render" + } + + ] + }, { name: "size", tests: [ @@ -1377,7 +1435,7 @@ ] }, { - name: "sep", + name: "sep / first / last", tests: [ { name: "sep helper with no body", @@ -1408,6 +1466,27 @@ }, expected: "3, 2, 1", message: "should sep helper in a async_iterator" + }, + { + name: "first helper", + source: "{#guests}{@first}Hello {/first}{.} {/guests}", + context: { guests: function() { return ["Alice", "Bob", "Charlie"]; } }, + expected: "Hello Alice Bob Charlie ", + message: "first helper should output on the first iteration only" + }, + { + name: "last helper", + source: "Hello {#guests}{@last}and {/last}{.} {/guests}", + context: { guests: function() { return ["Alice", "Bob", "Charlie"]; } }, + expected: "Hello Alice Bob and Charlie ", + message: "last helper should output on the last iteration only" + }, + { + name: "first / last / sep combo", + source: "{#guests}{@first}Hello {/first}{@last}and {/last}{.}{@last}!{/last}{@sep}, {/sep}{/guests}", + context: { guests: function() { return ["Alice", "Bob", "Charlie"]; } }, + expected: "Hello Alice, Bob, and Charlie!", + message: "first, last, and sep helpers should operate together" } ] }