diff --git a/lib/utils.js b/lib/utils.js index ebe8867e..f778daae 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -12,7 +12,7 @@ var hexTable = (function () { var has = Object.prototype.hasOwnProperty; exports.arrayToObject = function (source, options) { - var obj = options.plainObjects ? Object.create(null) : {}; + var obj = options && options.plainObjects ? Object.create(null) : {}; for (var i = 0; i < source.length; ++i) { if (typeof source[i] !== 'undefined') { obj[i] = source[i]; @@ -22,16 +22,30 @@ exports.arrayToObject = function (source, options) { return obj; }; -exports.merge = function (target, source, options) { +var isArray = Array.isArray; + +var arrayToObject = function arrayToObject(source, options) { + var obj = options && options.plainObjects ? Object.create(null) : {}; + for (var i = 0; i < source.length; ++i) { + if (typeof source[i] !== 'undefined') { + obj[i] = source[i]; + } + } + + return obj; +}; + +exports.merge = function merge(target, source, options) { + /* eslint no-param-reassign: 0 */ if (!source) { return target; } if (typeof source !== 'object') { - if (Array.isArray(target)) { + if (isArray(target)) { target.push(source); - } else if (typeof target === 'object') { - if (options.plainObjects || options.allowPrototypes || !has.call(Object.prototype, source)) { + } else if (target && typeof target === 'object') { + if ((options && (options.plainObjects || options.allowPrototypes)) || !has.call(Object.prototype, source)) { target[source] = true; } } else { @@ -41,20 +55,36 @@ exports.merge = function (target, source, options) { return target; } - if (typeof target !== 'object') { + if (!target || typeof target !== 'object') { return [target].concat(source); } var mergeTarget = target; - if (Array.isArray(target) && !Array.isArray(source)) { - mergeTarget = exports.arrayToObject(target, options); + if (isArray(target) && !isArray(source)) { + mergeTarget = arrayToObject(target, options); + } + + if (isArray(target) && isArray(source)) { + source.forEach(function (item, i) { + if (has.call(target, i)) { + var targetItem = target[i]; + if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') { + target[i] = merge(targetItem, item, options); + } else { + target.push(item); + } + } else { + target[i] = item; + } + }); + return target; } return Object.keys(source).reduce(function (acc, key) { var value = source[key]; if (has.call(acc, key)) { - acc[key] = exports.merge(acc[key], value, options); + acc[key] = merge(acc[key], value, options); } else { acc[key] = value; } @@ -132,7 +162,7 @@ exports.compact = function (obj, references) { refs.push(obj); - if (Array.isArray(obj)) { + if (isArray(obj)) { var compacted = []; for (var i = 0; i < obj.length; ++i) { diff --git a/test/utils.js b/test/utils.js index 4a8d8246..999f860d 100644 --- a/test/utils.js +++ b/test/utils.js @@ -5,5 +5,21 @@ var utils = require('../lib/utils'); test('merge()', function (t) { t.deepEqual(utils.merge({ a: 'b' }, { a: 'c' }), { a: ['b', 'c'] }, 'merges two objects with the same key'); + + var oneMerged = utils.merge({ foo: 'bar' }, { foo: { first: '123' } }); + t.deepEqual(oneMerged, { foo: ['bar', { first: '123' }] }, 'merges a standalone and an object into an array'); + + var twoMerged = utils.merge({ foo: ['bar', { first: '123' }] }, { foo: { second: '456' } }); + t.deepEqual(twoMerged, { foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }, 'merges a standalone and two objects into an array'); + + var sandwiched = utils.merge({ foo: ['bar', { first: '123', second: '456' }] }, { foo: 'baz' }); + t.deepEqual(sandwiched, { foo: ['bar', { first: '123', second: '456' }, 'baz'] }, 'merges an object sandwiched by two standalones into an array'); + + var nestedArrays = utils.merge({ foo: ['baz'] }, { foo: ['bar', 'xyzzy'] }); + t.deepEqual(nestedArrays, { foo: ['baz', 'bar', 'xyzzy'] }); + + var noOptionsNonObjectSource = utils.merge({ foo: 'baz' }, 'bar'); + t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true }); + t.end(); });