diff --git a/lodash.js b/lodash.js index c8443e7c0..57e4bfb44 100644 --- a/lodash.js +++ b/lodash.js @@ -745,14 +745,14 @@ * `chain`, `chunk`, `compact`, `compose`, `concat`, `constant`, `countBy`, * `create`, `curry`, `debounce`, `defaults`, `defer`, `delay`, `difference`, * `drop`, `dropRight`, `dropRightWhile`, `dropWhile`, `filter`, `flatten`, - * `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, - * `functions`, `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, - * `invoke`, `keys`, `keysIn`, `map`, `mapValues`, `matches`, `memoize`, `merge`, - * `mixin`, `negate`, `noop`, `omit`, `once`, `pairs`, `partial`, `partialRight`, - * `partition`, `pick`, `pluck`, `property`, `pull`, `pullAt`, `push`, `range`, - * `reject`, `remove`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, `sortBy`, - * `splice`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, `tap`, - * `throttle`, `times`, `toArray`, `transform`, `union`, `uniq`, `unshift`, + * `flattenDeep`, `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, + * `forOwnRight`, `functions`, `groupBy`, `indexBy`, `initial`, `intersection`, + * `invert`, `invoke`, `keys`, `keysIn`, `map`, `mapValues`, `matches`, `memoize`, + * `merge`, `mixin`, `negate`, `noop`, `omit`, `once`, `pairs`, `partial`, + * `partialRight`, `partition`, `pick`, `pluck`, `property`, `pull`, `pullAt`, + * `push`, `range`, `reject`, `remove`, `rest`, `reverse`, `shuffle`, `slice`, + * `sort`, `sortBy`, `splice`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, + * `tap`, `throttle`, `times`, `toArray`, `transform`, `union`, `uniq`, `unshift`, * `unzip`, `values`, `valuesIn`, `where`, `without`, `wrap`, `xor`, `zip`, * and `zipObject` * @@ -3466,6 +3466,24 @@ return baseFlatten(array, isDeep); } + /** + * Recursively flattens a nested array. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to recursively flatten. + * @returns {Array} Returns the new flattened array. + * @example + * + * _.flattenDeep([1, [2], [3, [[4]]]]); + * // => [1, 2, 3, 4]; + */ + function flattenDeep(array) { + var length = array ? array.length : 0; + return length ? baseFlatten(array, true) : []; + } + /** * Gets the index at which the first occurrence of `value` is found in `array` * using strict equality for comparisons, i.e. `===`. If `fromIndex` is negative, @@ -8092,6 +8110,7 @@ * @param {RegExp} [options.interpolate] The "interpolate" delimiter. * @param {string} [options.sourceURL] The sourceURL of the template's compiled source. * @param {string} [options.variable] The data object variable name. + * @param- {Object} [otherOptions] Enables the legacy `options` param signature. * @returns {Function} Returns the compiled template function. * @example * @@ -8159,13 +8178,13 @@ * };\ * '); */ - function template(string, options) { + function template(string, options, otherOptions) { // based on John Resig's `tmpl` implementation // http://ejohn.org/blog/javascript-micro-templating/ // and Laura Doktorova's doT.js // https://github.com/olado/doT var settings = lodash.templateSettings; - options = assign({}, options, settings, assignOwnDefaults); + options = assign({}, otherOptions || options, settings, assignOwnDefaults); string = String(string == null ? '' : string); var imports = assign({}, options.imports, settings.imports, assignOwnDefaults), @@ -9067,6 +9086,7 @@ lodash.dropWhile = dropWhile; lodash.filter = filter; lodash.flatten = flatten; + lodash.flattenDeep = flattenDeep; lodash.forEach = forEach; lodash.forEachRight = forEachRight; lodash.forIn = forIn; diff --git a/test/test.js b/test/test.js index ef5976860..857c0fb03 100644 --- a/test/test.js +++ b/test/test.js @@ -3700,7 +3700,7 @@ /*--------------------------------------------------------------------------*/ - QUnit.module('lodash.flatten'); + QUnit.module('flatten methods'); (function() { var args = arguments; @@ -3710,80 +3710,117 @@ deepEqual(_.flatten(array), [['a'], ['b']]); }); - test('should work with `isDeep`', 1, function() { - var array = [[['a']], [['b']]]; - deepEqual(_.flatten(array, true), ['a', 'b']); + test('should work with `isDeep`', 2, function() { + var array = [[['a']], [['b']]], + expected = ['a', 'b']; + + deepEqual(_.flatten(array, true), expected); + deepEqual(_.flattenDeep(array), expected); }); - test('should flatten `arguments` objects', 1, function() { - deepEqual(_.flatten([args, args]), [1, 2, 3, 1, 2, 3]); + test('should flatten `arguments` objects', 3, function() { + var array = [args, [args]], + expected = [1, 2, 3, args]; + + deepEqual(_.flatten(array), expected); + + expected = [1, 2, 3, 1, 2, 3]; + deepEqual(_.flatten(array, true), expected); + deepEqual(_.flattenDeep(array), expected); }); - test('should perform a shallow flatten when used as a callback for `_.map`', 1, function() { + test('should execute without options when used as a callback for `_.map`', 2, function() { var array = [[[['a']]], [[['b']]]]; + deepEqual(_.map(array, _.flatten), [[['a']], [['b']]]); + deepEqual(_.map(array, _.flattenDeep), [['a'], ['b']]); }); - test('should treat sparse arrays as dense', 4, function() { + test('should treat sparse arrays as dense', 6, function() { var array = [[1, 2, 3], Array(3)], expected = [1, 2, 3]; expected.push(undefined, undefined, undefined); - _.each([_.flatten(array), _.flatten(array, true)], function(actual) { + _.each([_.flatten(array), _.flatten(array, true), _.flattenDeep(array)], function(actual) { deepEqual(actual, expected); ok('4' in actual); }); }); - test('should work with extremely large arrays', 1, function() { + test('should work with extremely large arrays', 3, function() { // test in modern browsers if (freeze) { - try { - var expected = Array(5e5), - actual = _.flatten([expected]); + var expected = Array(5e5); - deepEqual(actual, expected); - } catch(e) { - ok(false); - } - } else { - skipTest(); - } - }); - - test('should work with empty arrays', 2, function() { - var array = [[], [[]], [[], [[[]]]]]; - - deepEqual(_.flatten(array), [[], [], [[[]]]]); - deepEqual(_.flatten(array, true), []); - }); - - test('should support flattening of nested arrays', 2, function() { - var array = [1, [2], [3, [4]]]; - - deepEqual(_.flatten(array), [1, 2, 3, [4]]); - deepEqual(_.flatten(array, true), [1, 2, 3, 4]); - }); - - test('should return an empty array for non array-like objects', 1, function() { - deepEqual(_.flatten({ 'a': 1 }), []); - }); - - test('should return a wrapped value when chaining', 4, function() { - if (!isNpm) { - var wrapped = _([1, [2], [3, [4]]]), - actual = wrapped.flatten(); - - ok(actual instanceof _); - deepEqual(actual.value(), [1, 2, 3, [4]]); - - actual = wrapped.flatten(true); - ok(actual instanceof _); - deepEqual(actual.value(), [1, 2, 3, 4]); + _.times(3, function(index) { + try { + if (index) { + var actual = actual == 1 ? _.flatten([expected], true) : _.flattenDeep([expected]); + } else { + actual = _.flatten(expected); + } + deepEqual(actual, expected); + } catch(e) { + ok(false); + } + }); } else { - skipTest(4); + skipTest(3); + } + }); + + test('should work with empty arrays', 3, function() { + var array = [[], [[]], [[], [[[]]]]], + expected = [[], [], [[[]]]]; + + deepEqual(_.flatten(array), expected); + + expected = []; + deepEqual(_.flatten(array, true), expected); + deepEqual(_.flattenDeep(array), expected); + }); + + test('should support flattening of nested arrays', 3, function() { + var array = [1, [2], [3, [4]]], + expected = [1, 2, 3, [4]]; + + deepEqual(_.flatten(array), expected); + + expected = [1, 2, 3, 4]; + deepEqual(_.flatten(array, true), expected); + deepEqual(_.flattenDeep(array), expected); + }); + + test('should return an empty array for non array-like objects', 3, function() { + var expected = []; + + deepEqual(_.flatten({ 'a': 1 }), expected); + deepEqual(_.flatten({ 'a': 1 }, true), expected); + deepEqual(_.flattenDeep({ 'a': 1 }), expected); + }); + + test('should return a wrapped value when chaining', 6, function() { + if (!isNpm) { + var wrapped = _([1, [2], [3, [4]]]), + actual = wrapped.flatten(), + expected = [1, 2, 3, [4]]; + + ok(actual instanceof _); + deepEqual(actual.value(), expected); + + expected = [1, 2, 3, 4]; + actual = wrapped.flatten(true); + ok(actual instanceof _); + deepEqual(actual.value(), expected); + + actual = wrapped.flattenDeep(); + ok(actual instanceof _); + deepEqual(actual.value(), expected); + } + else { + skipTest(6); } }); }(1, 2, 3)); @@ -9804,36 +9841,40 @@ strictEqual(compiled({ 'value': true }), 'yap'); }); - test('should work with custom `_.templateSettings` delimiters', 1, function() { - var settings = _.clone(_.templateSettings); + test('should work with custom delimiters', 2, function() { + _.times(2, function(index) { + var settingsClone = _.clone(_.templateSettings); - _.assign(_.templateSettings, { - 'escape': /\{\{-([\s\S]+?)\}\}/g, - 'evaluate': /\{\{([\s\S]+?)\}\}/g, - 'interpolate': /\{\{=([\s\S]+?)\}\}/g + var settings = _.assign(index ? _.templateSettings : {}, { + 'escape': /\{\{-([\s\S]+?)\}\}/g, + 'evaluate': /\{\{([\s\S]+?)\}\}/g, + 'interpolate': /\{\{=([\s\S]+?)\}\}/g + }); + + var compiled = _.template('', index ? null : settings), + expected = ''; + + strictEqual(compiled({ 'collection': ['a & A', 'b & B'] }), expected); + _.assign(_.templateSettings, settingsClone); }); - - var compiled = _.template(''), - expected = ''; - - strictEqual(compiled({ 'collection': ['a & A', 'b & B'] }), expected); - _.assign(_.templateSettings, settings); }); - test('should work with `_.templateSettings` delimiters containing special characters', 1, function() { - var settings = _.clone(_.templateSettings); + test('should work with custom delimiters containing special characters', 2, function() { + _.times(2, function(index) { + var settingsClone = _.clone(_.templateSettings); - _.assign(_.templateSettings, { - 'escape': /<\?-([\s\S]+?)\?>/g, - 'evaluate': /<\?([\s\S]+?)\?>/g, - 'interpolate': /<\?=([\s\S]+?)\?>/g + var settings = _.assign(index ? _.templateSettings : {}, { + 'escape': /<\?-([\s\S]+?)\?>/g, + 'evaluate': /<\?([\s\S]+?)\?>/g, + 'interpolate': /<\?=([\s\S]+?)\?>/g + }); + + var compiled = _.template('', index ? null : settings), + expected = ''; + + strictEqual(compiled({ 'collection': ['a & A', 'b & B'] }), expected); + _.assign(_.templateSettings, settingsClone); }); - - var compiled = _.template(''), - expected = ''; - - strictEqual(compiled({ 'collection': ['a & A', 'b & B'] }), expected); - _.assign(_.templateSettings, settings); }); test('should work with no delimiters', 1, function() { @@ -9842,9 +9883,7 @@ }); test('should support the "imports" option', 1, function() { - var options = { 'imports': { 'a': 1 } }, - compiled = _.template('<%= a %>', options); - + var compiled = _.template('<%= a %>', { 'imports': { 'a': 1 } }); strictEqual(compiled({}), '1'); }); @@ -9863,6 +9902,11 @@ } }); + test('should support the legacy `options` param signature', 1, function() { + var compiled = _.template('<%= data.a %>', null, { 'variable': 'data' }); + strictEqual(compiled({ 'a': 1 }), '1'); + }); + test('should use a `with` statement by default', 1, function() { var compiled = _.template('<%= index %><%= collection[index] %><% _.each(collection, function(value, index) { %><%= index %><% }); %>'), actual = compiled({ 'index': 1, 'collection': ['a', 'b', 'c'] }); @@ -11677,7 +11721,7 @@ var acceptFalsey = _.difference(allMethods, rejectFalsey); - test('should accept falsey arguments', 192, function() { + test('should accept falsey arguments', 193, function() { var emptyArrays = _.map(falsey, _.constant([])), isExposed = '_' in root, oldDash = root._;