diff --git a/.travis.yml b/.travis.yml index 239240b..0922f09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,6 @@ notifications: on_failure: change node_js: - - 8 + - 14 + - 12 + - 10 diff --git a/Gruntfile.coffee b/Gruntfile.coffee deleted file mode 100644 index 4f36d71..0000000 --- a/Gruntfile.coffee +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = (grunt) -> - grunt.initConfig - pkg: grunt.file.readJSON('package.json') - - coffee: - lib: - expand: true - cwd: 'src' - src: ['*.coffee'] - dest: 'lib' - ext: '.js' - - dist: - expand: true - cwd: 'src' - src: ['*.coffee'] - dest: 'dist' - ext: '.js' - - coffeelint: - options: - no_empty_param_list: - level: 'error' - max_line_length: - level: 'ignore' - - src: ['src/*.coffee'] - test: ['spec/*.coffee'] - - shell: - test: - command: 'node node_modules/jasmine-focused/bin/jasmine-focused --captureExceptions --coffee spec' - options: - stdout: true - stderr: true - failOnError: true - - grunt.loadNpmTasks('grunt-contrib-coffee') - grunt.loadNpmTasks('grunt-shell') - grunt.loadNpmTasks('grunt-coffeelint') - - grunt.registerTask 'clean', -> require('rimraf').sync('lib') - grunt.registerTask('lint', ['coffeelint:src', 'coffeelint:test']) - grunt.registerTask('bower', ['lint', 'coffee:dist']) - grunt.registerTask('default', ['coffee:lib', 'coffeelint']) - grunt.registerTask('test', ['default', 'coffeelint:test', 'shell:test']) diff --git a/appveyor.yml b/appveyor.yml index ebd3e68..ab37c36 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,10 @@ -image: Visual Studio 2015 +image: Visual Studio 2017 environment: - nodejs_version: "6" + nodejs_version: + - 10 + - 12 + - 14 platform: - x86 diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..0d779b8 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,20 @@ +let presets = []; + +let plugins = [ + ["@babel/plugin-proposal-optional-chaining", { loose: false }], + ["@babel/plugin-proposal-nullish-coalescing-operator", { loose: false }], +]; + +if (process.env.BABEL_ENV === "development") { + plugins.push(...[ + "@babel/plugin-transform-modules-commonjs", + "@babel/plugin-proposal-export-namespace-from", + ]); +} + +module.exports = { + presets: presets, + plugins: plugins, + exclude: "node_modules/**", + sourceMap: true, +}; diff --git a/dist/underscore-plus.js b/dist/underscore-plus.js index 32eb482..9b34617 100644 --- a/dist/underscore-plus.js +++ b/dist/underscore-plus.js @@ -1,522 +1,645 @@ -(function() { - var isEqual, isPlainObject, macModifierKeyMap, nonMacModifierKeyMap, plus, shiftKeyMap, splitKeyPath, _, - __slice = [].slice; - - _ = require('underscore'); - - macModifierKeyMap = { - cmd: '\u2318', - ctrl: '\u2303', - alt: '\u2325', - option: '\u2325', - shift: '\u21e7', - enter: '\u23ce', - left: '\u2190', - right: '\u2192', - up: '\u2191', - down: '\u2193' - }; +"use strict"; - nonMacModifierKeyMap = { - cmd: 'Cmd', - ctrl: 'Ctrl', - alt: 'Alt', - option: 'Alt', - shift: 'Shift', - enter: 'Enter', - left: 'Left', - right: 'Right', - up: 'Up', - down: 'Down' - }; +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _exportNames = { + adviseBefore: true, + camelize: true, + capitalize: true, + compactObject: true, + dasherize: true, + deepClone: true, + deepExtend: true, + deepContains: true, + endsWith: true, + escapeAttribute: true, + escapeRegExp: true, + humanizeEventName: true, + humanizeKey: true, + humanizeKeystroke: true, + isSubset: true, + losslessInvert: true, + mapObject: true, + multiplyString: true, + pluralize: true, + remove: true, + setValueForKeyPath: true, + hasKeyPath: true, + spliceWithArray: true, + sum: true, + uncamelcase: true, + undasherize: true, + underscore: true, + valueForKeyPath: true, + isEqual: true, + isEqualForProperties: true +}; +exports.adviseBefore = adviseBefore; +exports.camelize = camelize; +exports.capitalize = capitalize; +exports.compactObject = compactObject; +exports.dasherize = dasherize; +exports.deepClone = deepClone; +exports.deepExtend = deepExtend; +exports.deepContains = deepContains; +exports.endsWith = endsWith; +exports.escapeAttribute = escapeAttribute; +exports.escapeRegExp = escapeRegExp; +exports.humanizeEventName = humanizeEventName; +exports.humanizeKey = humanizeKey; +exports.humanizeKeystroke = humanizeKeystroke; +exports.isSubset = isSubset; +exports.losslessInvert = losslessInvert; +exports.mapObject = mapObject; +exports.multiplyString = multiplyString; +exports.pluralize = pluralize; +exports.remove = remove; +exports.setValueForKeyPath = setValueForKeyPath; +exports.hasKeyPath = hasKeyPath; +exports.spliceWithArray = spliceWithArray; +exports.sum = sum; +exports.uncamelcase = uncamelcase; +exports.undasherize = undasherize; +exports.underscore = underscore; +exports.valueForKeyPath = valueForKeyPath; +exports.isEqual = isEqual; +exports.isEqualForProperties = isEqualForProperties; - shiftKeyMap = { - '~': '`', - '_': '-', - '+': '=', - '|': '\\', - '{': '[', - '}': ']', - ':': ';', - '"': '\'', - '<': ',', - '>': '.', - '?': '/' - }; +var _underscore = require("underscore"); - splitKeyPath = function(keyPath) { - var char, i, keyPathArray, startIndex, _i, _len; - startIndex = 0; - keyPathArray = []; - if (keyPath == null) { - return keyPathArray; +Object.keys(_underscore).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _underscore[key]; } - for (i = _i = 0, _len = keyPath.length; _i < _len; i = ++_i) { - char = keyPath[i]; - if (char === '.' && (i === 0 || keyPath[i - 1] !== '\\')) { - keyPathArray.push(keyPath.substring(startIndex, i)); - startIndex = i + 1; - } - } - keyPathArray.push(keyPath.substr(startIndex, keyPath.length)); + }); +}); +// import * as _ from 'underscore' // if we needed all +const macModifierKeyMap = { + cmd: '\u2318', + ctrl: '\u2303', + alt: '\u2325', + option: '\u2325', + shift: '\u21e7', + enter: '\u23ce', + left: '\u2190', + right: '\u2192', + up: '\u2191', + down: '\u2193' +}; +const nonMacModifierKeyMap = { + cmd: 'Cmd', + ctrl: 'Ctrl', + alt: 'Alt', + option: 'Alt', + shift: 'Shift', + enter: 'Enter', + left: 'Left', + right: 'Right', + up: 'Up', + down: 'Down' +}; // Human key combos should always explicitly state the shift key. This map is a disambiguator. +// 'shift-version': 'no-shift-version' + +const shiftKeyMap = { + '~': '`', + '_': '-', + '+': '=', + '|': '\\', + '{': '[', + '}': ']', + ':': ';', + '"': '\'', + '<': ',', + '>': '.', + '?': '/' +}; + +function splitKeyPath(keyPath) { + let startIndex = 0; + const keyPathArray = []; + + if (keyPath == null) { return keyPathArray; - }; + } + + for (let i = 0; i < keyPath.length; i++) { + const char = keyPath[i]; + + if (char === '.' && (i === 0 || keyPath[i - 1] !== '\\')) { + keyPathArray.push(keyPath.substring(startIndex, i)); + startIndex = i + 1; + } + } + + keyPathArray.push(keyPath.substr(startIndex, keyPath.length)); + return keyPathArray; +} - isPlainObject = function(value) { - return _.isObject(value) && !_.isArray(value); +const isPlainObject = value => (0, _underscore.isObject)(value) && !(0, _underscore.isArray)(value); + +function adviseBefore(object, methodName, advice) { + const original = object[methodName]; + return object[methodName] = function (...args) { + if (advice.apply(this, args) !== false) { + return original.apply(this, args); + } }; +} - plus = { - adviseBefore: function(object, methodName, advice) { - var original; - original = object[methodName]; - return object[methodName] = function() { - var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - if (advice.apply(this, args) !== false) { - return original.apply(this, args); - } - }; - }, - camelize: function(string) { - if (string) { - return string.replace(/[_-]+(\w)/g, function(m) { - return m[1].toUpperCase(); - }); - } else { - return ''; - } - }, - capitalize: function(word) { - if (!word) { - return ''; - } - if (word.toLowerCase() === 'github') { - return 'GitHub'; - } else { - return word[0].toUpperCase() + word.slice(1); - } - }, - compactObject: function(object) { - var key, newObject, value; - newObject = {}; - for (key in object) { - value = object[key]; - if (value != null) { - newObject[key] = value; - } - } - return newObject; - }, - dasherize: function(string) { - if (!string) { - return ''; - } - string = string[0].toLowerCase() + string.slice(1); - return string.replace(/([A-Z])|(_)/g, function(m, letter) { - if (letter) { - return "-" + letter.toLowerCase(); - } else { - return "-"; - } - }); - }, - deepClone: function(object) { - if (_.isArray(object)) { - return object.map(function(value) { - return plus.deepClone(value); - }); - } else if (_.isObject(object) && !_.isFunction(object)) { - return plus.mapObject(object, (function(_this) { - return function(key, value) { - return [key, plus.deepClone(value)]; - }; - })(this)); - } else { - return object; - } - }, - deepExtend: function(target) { - var i, key, object, result, _i, _len, _ref; - result = target; - i = 0; - while (++i < arguments.length) { - object = arguments[i]; - if (isPlainObject(result) && isPlainObject(object)) { - _ref = Object.keys(object); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - key = _ref[_i]; - result[key] = plus.deepExtend(result[key], object[key]); - } - } else { - result = plus.deepClone(object); - } - } - return result; - }, - deepContains: function(array, target) { - var object, _i, _len; - if (array == null) { - return false; - } - for (_i = 0, _len = array.length; _i < _len; _i++) { - object = array[_i]; - if (_.isEqual(object, target)) { - return true; - } - } - return false; - }, - endsWith: function(string, suffix) { - if (suffix == null) { - suffix = ''; - } - if (string) { - return string.indexOf(suffix, string.length - suffix.length) !== -1; - } else { - return false; - } - }, - escapeAttribute: function(string) { - if (string) { - return string.replace(/"/g, '"').replace(/\n/g, '').replace(/\\/g, '-'); - } else { - return ''; - } - }, - escapeRegExp: function(string) { - if (string) { - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } else { - return ''; - } - }, - humanizeEventName: function(eventName, eventDoc) { - var event, namespace, namespaceDoc, _ref; - _ref = eventName.split(':'), namespace = _ref[0], event = _ref[1]; - if (event == null) { - return plus.undasherize(namespace); - } - namespaceDoc = plus.undasherize(namespace); - if (eventDoc == null) { - eventDoc = plus.undasherize(event); - } - return "" + namespaceDoc + ": " + eventDoc; - }, - humanizeKey: function(key, platform) { - var modifierKeyMap; - if (platform == null) { - platform = process.platform; - } - if (!key) { - return key; - } - modifierKeyMap = platform === 'darwin' ? macModifierKeyMap : nonMacModifierKeyMap; - if (modifierKeyMap[key]) { - return modifierKeyMap[key]; - } else if (key.length === 1 && (shiftKeyMap[key] != null)) { - return [modifierKeyMap.shift, shiftKeyMap[key]]; - } else if (key.length === 1 && key === key.toUpperCase() && key.toUpperCase() !== key.toLowerCase()) { - return [modifierKeyMap.shift, key.toUpperCase()]; - } else if (key.length === 1 || /f[0-9]{1,2}/.test(key)) { - return key.toUpperCase(); - } else { - if (platform === 'darwin') { - return key; - } else { - return plus.capitalize(key); - } - } - }, - humanizeKeystroke: function(keystroke, platform) { - var humanizedKeystrokes, index, key, keys, keystrokes, splitKeystroke, _i, _j, _len, _len1; - if (platform == null) { - platform = process.platform; - } - if (!keystroke) { - return keystroke; - } - keystrokes = keystroke.split(' '); - humanizedKeystrokes = []; - for (_i = 0, _len = keystrokes.length; _i < _len; _i++) { - keystroke = keystrokes[_i]; - keys = []; - splitKeystroke = keystroke.split('-'); - for (index = _j = 0, _len1 = splitKeystroke.length; _j < _len1; index = ++_j) { - key = splitKeystroke[index]; - if (key === '' && splitKeystroke[index - 1] === '') { - key = '-'; - } - if (key) { - keys.push(plus.humanizeKey(key, platform)); - } - } - keys = _.uniq(_.flatten(keys)); - if (platform === 'darwin') { - keys = keys.join(''); - } else { - keys = keys.join('+'); - } - humanizedKeystrokes.push(keys); - } - return humanizedKeystrokes.join(' '); - }, - isSubset: function(potentialSubset, potentialSuperset) { - return _.every(potentialSubset, function(element) { - return _.include(potentialSuperset, element); - }); - }, - losslessInvert: function(hash) { - var inverted, key, value; - inverted = {}; - for (key in hash) { - value = hash[key]; - if (inverted[value] == null) { - inverted[value] = []; - } - inverted[value].push(key); - } - return inverted; - }, - mapObject: function(object, iterator) { - var key, newObject, value, _i, _len, _ref, _ref1; - newObject = {}; - _ref = Object.keys(object); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - key = _ref[_i]; - _ref1 = iterator(key, object[key]), key = _ref1[0], value = _ref1[1]; - newObject[key] = value; - } - return newObject; - }, - multiplyString: function(string, n) { - var finalString, i; - finalString = ""; - i = 0; - while (i < n) { - finalString += string; - i++; - } - return finalString; - }, - pluralize: function(count, singular, plural) { - if (count == null) { - count = 0; - } - if (plural == null) { - plural = singular + 's'; - } - if (count === 1) { - return "" + count + " " + singular; - } else { - return "" + count + " " + plural; - } - }, - remove: function(array, element) { - var index; - index = array.indexOf(element); - if (index >= 0) { - array.splice(index, 1); - } - return array; - }, - setValueForKeyPath: function(object, keyPath, value) { - var key, keys; - keys = splitKeyPath(keyPath); - while (keys.length > 1) { - key = keys.shift(); - if (object[key] == null) { - object[key] = {}; - } - object = object[key]; - } - if (value != null) { - return object[keys.shift()] = value; - } else { - return delete object[keys.shift()]; - } - }, - hasKeyPath: function(object, keyPath) { - var key, keys, _i, _len; - keys = splitKeyPath(keyPath); - for (_i = 0, _len = keys.length; _i < _len; _i++) { - key = keys[_i]; - if (!object.hasOwnProperty(key)) { - return false; - } - object = object[key]; +function camelize(string) { + if (string) { + return string.replace(/[_-]+(\w)/g, m => m[1].toUpperCase()); + } else { + return ''; + } +} + +function capitalize(word) { + if (!word) { + return ''; + } + + if (word.toLowerCase() === 'github') { + return 'GitHub'; + } else { + return word[0].toUpperCase() + word.slice(1); + } +} + +function compactObject(object) { + const newObject = {}; + + for (let key in object) { + const value = object[key]; + + if (value != null) { + newObject[key] = value; + } + } + + return newObject; +} + +function dasherize(string) { + if (!string) { + return ''; + } + + string = string[0].toLowerCase() + string.slice(1); + return string.replace(/([A-Z])|(_)/g, function (m, letter) { + if (letter) { + return "-" + letter.toLowerCase(); + } else { + return "-"; + } + }); +} // Deep clones the given JSON object. +// +// `object` - The JSON object to clone. +// +// Returns a deep clone of the JSON object. + + +function deepClone(object) { + if ((0, _underscore.isArray)(object)) { + return object.map(value => deepClone(value)); + } else if ((0, _underscore.isObject)(object) && !(0, _underscore.isFunction)(object)) { + return mapObject(object, (key, value) => [key, deepClone(value)]); + } else { + return object; + } +} + +function deepExtend(target) { + let result = target; + let i = 0; + + while (++i < arguments.length) { + const object = arguments[i]; + + if (isPlainObject(result) && isPlainObject(object)) { + const keys = Object.keys(object); + + for (let key of keys) { + result[key] = deepExtend(result[key], object[key]); } + } else { + result = deepClone(object); + } + } + + return result; +} + +function deepContains(array, target) { + if (array == null) { + return false; + } + + for (let object of array) { + if ((0, _underscore.isEqual)(object, target)) { return true; - }, - spliceWithArray: function(originalArray, start, length, insertedArray, chunkSize) { - var chunkStart, _i, _ref, _results; - if (chunkSize == null) { - chunkSize = 100000; - } - if (insertedArray.length < chunkSize) { - return originalArray.splice.apply(originalArray, [start, length].concat(__slice.call(insertedArray))); - } else { - originalArray.splice(start, length); - _results = []; - for (chunkStart = _i = 0, _ref = insertedArray.length; chunkSize > 0 ? _i <= _ref : _i >= _ref; chunkStart = _i += chunkSize) { - _results.push(originalArray.splice.apply(originalArray, [start + chunkStart, 0].concat(__slice.call(insertedArray.slice(chunkStart, chunkStart + chunkSize))))); - } - return _results; - } - }, - sum: function(array) { - var elt, sum, _i, _len; - sum = 0; - for (_i = 0, _len = array.length; _i < _len; _i++) { - elt = array[_i]; - sum += elt; - } - return sum; - }, - uncamelcase: function(string) { - var result; - if (!string) { - return ''; - } - result = string.replace(/([A-Z])|_+/g, function(match, letter) { - if (letter == null) { - letter = ''; - } - return " " + letter; - }); - return plus.capitalize(result.trim()); - }, - undasherize: function(string) { - if (string) { - return string.split('-').map(plus.capitalize).join(' '); - } else { - return ''; - } - }, - underscore: function(string) { - if (!string) { - return ''; - } - string = string[0].toLowerCase() + string.slice(1); - return string.replace(/([A-Z])|-+/g, function(match, letter) { - if (letter == null) { - letter = ''; - } - return "_" + (letter.toLowerCase()); - }); - }, - valueForKeyPath: function(object, keyPath) { - var key, keys, _i, _len; - keys = splitKeyPath(keyPath); - for (_i = 0, _len = keys.length; _i < _len; _i++) { - key = keys[_i]; - object = object[key]; - if (object == null) { - return; - } - } - return object; - }, - isEqual: function(a, b, aStack, bStack) { - if (_.isArray(aStack) && _.isArray(bStack)) { - return isEqual(a, b, aStack, bStack); - } else { - return isEqual(a, b); + } + } + + return false; +} + +function endsWith(string, suffix) { + if (suffix == null) { + suffix = ''; + } + + if (string) { + return string.indexOf(suffix, string.length - suffix.length) !== -1; + } else { + return false; + } +} + +function escapeAttribute(string) { + if (string) { + return string.replace(/"/g, '"').replace(/\n/g, '').replace(/\\/g, '-'); + } else { + return ''; + } +} + +function escapeRegExp(string) { + if (string) { + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } else { + return ''; + } +} + +function humanizeEventName(eventName, eventDoc) { + const [namespace, event] = eventName.split(':'); + + if (event == null) { + return undasherize(namespace); + } + + const namespaceDoc = undasherize(namespace); + + if (eventDoc == null) { + eventDoc = undasherize(event); + } + + return `${namespaceDoc}: ${eventDoc}`; +} + +function humanizeKey(key, platform = process.platform) { + if (!key) { + return key; + } + + const modifierKeyMap = platform === 'darwin' ? macModifierKeyMap : nonMacModifierKeyMap; + + if (modifierKeyMap[key]) { + return modifierKeyMap[key]; + } else if (key.length === 1 && shiftKeyMap[key] != null) { + return [modifierKeyMap.shift, shiftKeyMap[key]]; + } else if (key.length === 1 && key === key.toUpperCase() && key.toUpperCase() !== key.toLowerCase()) { + return [modifierKeyMap.shift, key.toUpperCase()]; + } else if (key.length === 1 || /f[0-9]{1,2}/.test(key)) { + return key.toUpperCase(); + } else { + if (platform === 'darwin') { + return key; + } else { + return capitalize(key); + } + } +} // Humanize the keystroke according to platform conventions. This method +// attempts to mirror the text the given keystroke would have if displayed in +// a system menu. +// +// keystroke - A String keystroke to humanize such as `ctrl-O`. +// platform - An optional String platform to humanize for (default: +// `process.platform`). +// +// Returns a humanized representation of the keystroke. + + +function humanizeKeystroke(keystroke, platform = process.platform) { + if (!keystroke) { + return keystroke; + } + + const keystrokes = keystroke.split(' '); + const humanizedKeystrokes = []; + + for (keystroke of keystrokes) { + let keys = []; + const splitKeystroke = keystroke.split('-'); + + for (let index = 0; index < splitKeystroke.length; index++) { + // Check for consecutive dashes such as cmd-- + let key = splitKeystroke[index]; + + if (key === '' && splitKeystroke[index - 1] === '') { + key = '-'; } - }, - isEqualForProperties: function() { - var a, b, properties, property, _i, _len; - a = arguments[0], b = arguments[1], properties = 3 <= arguments.length ? __slice.call(arguments, 2) : []; - for (_i = 0, _len = properties.length; _i < _len; _i++) { - property = properties[_i]; - if (!_.isEqual(a[property], b[property])) { - return false; - } + + if (key) { + keys.push(humanizeKey(key, platform)); } - return true; } - }; - isEqual = function(a, b, aStack, bStack) { - var aCtor, aCtorValid, aElement, aKeyCount, aValue, bCtor, bCtorValid, bKeyCount, bValue, equal, i, key, stackIndex, _i, _len; - if (aStack == null) { - aStack = []; + keys = (0, _underscore.uniq)((0, _underscore.flatten)(keys)); + + if (platform === 'darwin') { + keys = keys.join(''); + } else { + keys = keys.join('+'); } - if (bStack == null) { - bStack = []; + + humanizedKeystrokes.push(keys); + } + + return humanizedKeystrokes.join(' '); +} + +function isSubset(potentialSubset, potentialSuperset) { + return (0, _underscore.every)(potentialSubset, element => (0, _underscore.include)(potentialSuperset, element)); +} + +function losslessInvert(hash) { + const inverted = {}; + + for (let key in hash) { + const value = hash[key]; + + if (inverted[value] == null) { + inverted[value] = []; } - if (a === b) { - return _.isEqual(a, b); + + inverted[value].push(key); + } + + return inverted; +} // Transform the given object into another object. +// +// `object` - The object to transform. +// `iterator` - +// A function that takes `(key, value)` arguments and returns a +// `[key, value]` tuple. +// +// Returns a new object based with the key/values returned by the iterator. + + +function mapObject(object, iterator) { + const newObject = {}; + const keys = Object.keys(object); + + for (let key of keys) { + let value; + [key, value] = iterator(key, object[key]); + newObject[key] = value; + } + + return newObject; +} + +function multiplyString(string, n) { + let finalString = ""; + let i = 0; + + while (i < n) { + finalString += string; + i++; + } + + return finalString; +} + +function pluralize(count = 0, singular, plural = singular + 's') { + if (count === 1) { + return `${count} ${singular}`; + } else { + return `${count} ${plural}`; + } +} + +function remove(array, element) { + const index = array.indexOf(element); + + if (index >= 0) { + array.splice(index, 1); + } + + return array; +} + +function setValueForKeyPath(object, keyPath, value) { + const keys = splitKeyPath(keyPath); + + while (keys.length > 1) { + const key = keys.shift(); + + if (object[key] == null) { + object[key] = {}; } - if (_.isFunction(a) || _.isFunction(b)) { - return _.isEqual(a, b); + + object = object[key]; + } + + if (value != null) { + object[keys.shift()] = value; + } else { + delete object[keys.shift()]; + } +} + +function hasKeyPath(object, keyPath) { + const keys = splitKeyPath(keyPath); + + for (let key of keys) { + if (!object.hasOwnProperty(key)) { + return false; } - stackIndex = aStack.length; - while (stackIndex--) { - if (aStack[stackIndex] === a) { - return bStack[stackIndex] === b; + + object = object[key]; + } + + return true; +} + +function spliceWithArray(originalArray, start, length, insertedArray, chunkSize = 100000) { + if (insertedArray.length < chunkSize) { + originalArray.splice(start, length, ...insertedArray); + } else { + originalArray.splice(start, length); + + for (let chunkStart = 0, end = insertedArray.length; chunkStart <= end; chunkStart += chunkSize) { + originalArray.splice(start + chunkStart, 0, ...insertedArray.slice(chunkStart, chunkStart + chunkSize)); + } + } +} + +function sum(array) { + let sum = 0; + + for (let elt of array) { + sum += elt; + } + + return sum; +} + +function uncamelcase(string) { + if (!string) { + return ''; + } + + const result = string.replace(/([A-Z])|_+/g, (match, letter = '') => ` ${letter}`); + return capitalize(result.trim()); +} + +function undasherize(string) { + if (string) { + return string.split('-').map(capitalize).join(' '); + } else { + return ''; + } +} + +function underscore(string) { + if (!string) { + return ''; + } + + string = string[0].toLowerCase() + string.slice(1); + return string.replace(/([A-Z])|-+/g, (match, letter = '') => `_${letter.toLowerCase()}`); +} + +function valueForKeyPath(object, keyPath) { + const keys = splitKeyPath(keyPath); + + for (let key of keys) { + object = object[key]; + + if (object == null) { + return; + } + } + + return object; +} + +function isEqual(a, b, aStack, bStack) { + if ((0, _underscore.isArray)(aStack) && (0, _underscore.isArray)(bStack)) { + return isEqual_(a, b, aStack, bStack); + } else { + return isEqual_(a, b); + } +} + +function isEqualForProperties(a, b, ...properties) { + // TODO is Array.from needed? + for (let property of Array.from(properties)) { + if (!(0, _underscore.isEqual)(a[property], b[property])) { + return false; + } + } + + return true; +} + +function isEqual_(a, b, aStack = [], bStack = []) { + if (a === b) { + return (0, _underscore.isEqual)(a, b); + } + + if ((0, _underscore.isFunction)(a) || (0, _underscore.isFunction)(b)) { + return (0, _underscore.isEqual)(a, b); + } + + let stackIndex = aStack.length; + + while (stackIndex--) { + if (aStack[stackIndex] === a) { + return bStack[stackIndex] === b; + } + } + + aStack.push(a); + bStack.push(b); + let equal = false; + + if ((0, _underscore.isFunction)(a === null || a === void 0 ? void 0 : a.isEqual)) { + equal = a.isEqual(b, aStack, bStack); + } else if ((0, _underscore.isFunction)(b === null || b === void 0 ? void 0 : b.isEqual)) { + equal = b.isEqual(a, bStack, aStack); + } else if ((0, _underscore.isArray)(a) && (0, _underscore.isArray)(b) && a.length === b.length) { + equal = true; + + for (let i = 0; i < a.length; i++) { + const aElement = a[i]; + + if (!isEqual_(aElement, b[i], aStack, bStack)) { + equal = false; + break; } } - aStack.push(a); - bStack.push(b); - equal = false; - if (_.isFunction(a != null ? a.isEqual : void 0)) { - equal = a.isEqual(b, aStack, bStack); - } else if (_.isFunction(b != null ? b.isEqual : void 0)) { - equal = b.isEqual(a, bStack, aStack); - } else if (_.isArray(a) && _.isArray(b) && a.length === b.length) { + } else if ((0, _underscore.isRegExp)(a) && (0, _underscore.isRegExp)(b)) { + equal = (0, _underscore.isEqual)(a, b); + } else if ((0, _underscore.isElement)(a) && (0, _underscore.isElement)(b)) { + equal = a === b; + } else if ((0, _underscore.isObject)(a) && (0, _underscore.isObject)(b)) { + const aCtor = a.constructor; + const bCtor = b.constructor; + const aCtorValid = (0, _underscore.isFunction)(aCtor) && aCtor instanceof aCtor; + const bCtorValid = (0, _underscore.isFunction)(bCtor) && bCtor instanceof bCtor; + + if (aCtor !== bCtor && !(aCtorValid && bCtorValid)) { + equal = false; + } else { + let key; + let aKeyCount = 0; equal = true; - for (i = _i = 0, _len = a.length; _i < _len; i = ++_i) { - aElement = a[i]; - if (!isEqual(aElement, b[i], aStack, bStack)) { + + for (key in a) { + const aValue = a[key]; + + if (!(0, _underscore.has)(a, key)) { + continue; + } + + aKeyCount++; + + if (!(0, _underscore.has)(b, key) || !isEqual_(aValue, b[key], aStack, bStack)) { equal = false; break; } } - } else if (_.isRegExp(a) && _.isRegExp(b)) { - equal = _.isEqual(a, b); - } else if (_.isElement(a) && _.isElement(b)) { - equal = a === b; - } else if (_.isObject(a) && _.isObject(b)) { - aCtor = a.constructor; - bCtor = b.constructor; - aCtorValid = _.isFunction(aCtor) && aCtor instanceof aCtor; - bCtorValid = _.isFunction(bCtor) && bCtor instanceof bCtor; - if (aCtor !== bCtor && !(aCtorValid && bCtorValid)) { - equal = false; - } else { - aKeyCount = 0; - equal = true; - for (key in a) { - aValue = a[key]; - if (!_.has(a, key)) { - continue; - } - aKeyCount++; - if (!(_.has(b, key) && isEqual(aValue, b[key], aStack, bStack))) { - equal = false; - break; - } - } - if (equal) { - bKeyCount = 0; - for (key in b) { - bValue = b[key]; - if (_.has(b, key)) { - bKeyCount++; - } + + if (equal) { + let bKeyCount = 0; + + for (key in b) { + const bValue = b[key]; + + if ((0, _underscore.has)(b, key)) { + bKeyCount++; } - equal = aKeyCount === bKeyCount; } + + equal = aKeyCount === bKeyCount; } - } else { - equal = _.isEqual(a, b); } - aStack.pop(); - bStack.pop(); - return equal; - }; - - module.exports = _.extend({}, _, plus); + } else { + equal = (0, _underscore.isEqual)(a, b); + } -}).call(this); + aStack.pop(); + bStack.pop(); + return equal; +} // TODO: Consider shorter variations of null checks: +// https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md#ds207-consider-shorter-variations-of-null-checks \ No newline at end of file diff --git a/package.json b/package.json index e205563..24f55fe 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,13 @@ ], "main": "./lib/underscore-plus.js", "scripts": { - "prepublish": "grunt clean coffee lint", - "test": "grunt test" + "clean": "shx rm -rf lib", + "test": "jasmine-focused --captureExceptions --coffee spec", + "lint": "coffeelint spec/*.coffee", + "bower": "shx mkdir -p dist && shx cp -r lib/*.js ./dist", + "babel": "npm run clean && shx cp -r src lib && cross-env NODE_ENV=development cross-env BABEL_ENV=development babel lib --out-dir lib && shx rm -f lib/*.mjs", + "build": "npm run babel", + "prepare": "npm run clean && npm run build && npm run bower" }, "repository": { "type": "git", @@ -24,15 +29,27 @@ "underscore" ], "dependencies": { - "underscore": "^1.9.1" + "underscore": "^1.10.2" }, "devDependencies": { + "@types/underscore": "^1.10.3", "jasmine-focused": "1.x", - "grunt-contrib-coffee": "~0.9.0", - "grunt-cli": "~0.1.8", - "grunt": "~0.4.1", - "grunt-shell": "~0.2.2", - "grunt-coffeelint": "0.0.6", - "rimraf": "~2.5.2" + "coffeelint": "^2.1.0", + "@babel/cli": "7.10.3", + "@babel/core": "7.10.3", + "@babel/plugin-proposal-export-namespace-from": "^7.10.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", + "@babel/plugin-proposal-optional-chaining": "^7.10.4", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "shx": "^0.3.2", + "cross-env": "^7.0.2" + }, + "coffeelintConfig": { + "no_empty_param_list": { + "level": "error" + }, + "max_line_length": { + "level": "ignore" + } } } diff --git a/spec/underscore-plus-spec.coffee b/spec/underscore-plus-spec.coffee index 615f772..ecc5e15 100644 --- a/spec/underscore-plus-spec.coffee +++ b/spec/underscore-plus-spec.coffee @@ -1,4 +1,4 @@ -_ = require '../src/underscore-plus' +_ = require '../lib/underscore-plus.js' describe "underscore extensions", -> describe "::adviseBefore(object, methodName, advice)", -> diff --git a/src/underscore-plus.coffee b/src/underscore-plus.coffee deleted file mode 100644 index 18cb5e5..0000000 --- a/src/underscore-plus.coffee +++ /dev/null @@ -1,366 +0,0 @@ -_ = require 'underscore' - -macModifierKeyMap = - cmd: '\u2318' - ctrl: '\u2303' - alt: '\u2325' - option: '\u2325' - shift: '\u21e7' - enter: '\u23ce' - left: '\u2190' - right: '\u2192' - up: '\u2191' - down: '\u2193' - -nonMacModifierKeyMap = - cmd: 'Cmd' - ctrl: 'Ctrl' - alt: 'Alt' - option: 'Alt' - shift: 'Shift' - enter: 'Enter' - left: 'Left' - right: 'Right' - up: 'Up' - down: 'Down' - -# Human key combos should always explicitly state the shift key. This map is a disambiguator. -# 'shift-version': 'no-shift-version' -shiftKeyMap = - '~': '`' - '_': '-' - '+': '=' - '|': '\\' - '{': '[' - '}': ']' - ':': ';' - '"': '\'' - '<': ',' - '>': '.' - '?': '/' - -splitKeyPath = (keyPath) -> - startIndex = 0 - keyPathArray = [] - return keyPathArray unless keyPath? - for char, i in keyPath - if char is '.' and (i is 0 or keyPath[i-1] != '\\') - keyPathArray.push keyPath.substring(startIndex, i) - startIndex = i + 1 - keyPathArray.push keyPath.substr(startIndex, keyPath.length) - keyPathArray - -isPlainObject = (value) -> - _.isObject(value) and not _.isArray(value) - -plus = - adviseBefore: (object, methodName, advice) -> - original = object[methodName] - object[methodName] = (args...) -> - unless advice.apply(this, args) == false - original.apply(this, args) - - camelize: (string) -> - if string - string.replace /[_-]+(\w)/g, (m) -> m[1].toUpperCase() - else - '' - - capitalize: (word) -> - return '' unless word - - if word.toLowerCase() is 'github' - 'GitHub' - else - word[0].toUpperCase() + word[1..] - - compactObject: (object) -> - newObject = {} - for key, value of object - newObject[key] = value if value? - newObject - - dasherize: (string) -> - return '' unless string - - string = string[0].toLowerCase() + string[1..] - string.replace /([A-Z])|(_)/g, (m, letter) -> - if letter - "-" + letter.toLowerCase() - else - "-" - - # Deep clones the given JSON object. - # - # `object` - The JSON object to clone. - # - # Returns a deep clone of the JSON object. - deepClone: (object) -> - if _.isArray(object) - object.map (value) -> plus.deepClone(value) - else if _.isObject(object) and not _.isFunction(object) - plus.mapObject object, (key, value) => [key, plus.deepClone(value)] - else - object - - deepExtend: (target) -> - result = target - i = 0 - while ++i < arguments.length - object = arguments[i] - if isPlainObject(result) and isPlainObject(object) - for key in Object.keys(object) - result[key] = plus.deepExtend(result[key], object[key]) - else - result = plus.deepClone(object) - result - - deepContains: (array, target) -> - return false unless array? - for object in array - return true if _.isEqual(object, target) - false - - endsWith: (string, suffix='') -> - if string - string.indexOf(suffix, string.length - suffix.length) isnt -1 - else - false - - escapeAttribute: (string) -> - if string - string.replace(/"/g, '"').replace(/\n/g, '').replace(/\\/g, '-') - else - '' - - escapeRegExp: (string) -> - if string - string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') - else - '' - - humanizeEventName: (eventName, eventDoc) -> - [namespace, event] = eventName.split(':') - return plus.undasherize(namespace) unless event? - - namespaceDoc = plus.undasherize(namespace) - eventDoc ?= plus.undasherize(event) - - "#{namespaceDoc}: #{eventDoc}" - - humanizeKey: (key, platform=process.platform) -> - return key unless key - - modifierKeyMap = if platform is 'darwin' then macModifierKeyMap else nonMacModifierKeyMap - - if modifierKeyMap[key] - modifierKeyMap[key] - else if key.length == 1 and shiftKeyMap[key]? - [modifierKeyMap.shift, shiftKeyMap[key]] - else if key.length == 1 and key == key.toUpperCase() and key.toUpperCase() != key.toLowerCase() - [modifierKeyMap.shift, key.toUpperCase()] - else if key.length == 1 or /f[0-9]{1,2}/.test(key) - key.toUpperCase() - else - if platform is 'darwin' - key - else - plus.capitalize(key) - - # Humanize the keystroke according to platform conventions. This method - # attempts to mirror the text the given keystroke would have if displayed in - # a system menu. - # - # keystroke - A String keystroke to humanize such as `ctrl-O`. - # platform - An optional String platform to humanize for (default: - # `process.platform`). - # - # Returns a humanized representation of the keystroke. - humanizeKeystroke: (keystroke, platform=process.platform) -> - return keystroke unless keystroke - - keystrokes = keystroke.split(' ') - humanizedKeystrokes = [] - for keystroke in keystrokes - keys = [] - splitKeystroke = keystroke.split('-') - for key, index in splitKeystroke - # Check for consecutive dashes such as cmd-- - key = '-' if key is '' and splitKeystroke[index-1] is '' - keys.push(plus.humanizeKey(key, platform)) if key - - keys = _.uniq(_.flatten(keys)) - if platform is 'darwin' - keys = keys.join('') - else - keys = keys.join('+') - humanizedKeystrokes.push(keys) - - humanizedKeystrokes.join(' ') - - isSubset: (potentialSubset, potentialSuperset) -> - _.every potentialSubset, (element) -> _.include(potentialSuperset, element) - - losslessInvert: (hash) -> - inverted = {} - for key, value of hash - inverted[value] ?= [] - inverted[value].push(key) - inverted - - # Transform the given object into another object. - # - # `object` - The object to transform. - # `iterator` - - # A function that takes `(key, value)` arguments and returns a - # `[key, value]` tuple. - # - # Returns a new object based with the key/values returned by the iterator. - mapObject: (object, iterator) -> - newObject = {} - for key in Object.keys(object) - [key, value] = iterator(key, object[key]) - newObject[key] = value - - newObject - - multiplyString: (string, n) -> - finalString = "" - i = 0 - while i < n - finalString += string - i++ - finalString - - pluralize: (count=0, singular, plural=singular+'s') -> - if count is 1 - "#{count} #{singular}" - else - "#{count} #{plural}" - - remove: (array, element) -> - index = array.indexOf(element) - array.splice(index, 1) if index >= 0 - array - - setValueForKeyPath: (object, keyPath, value) -> - keys = splitKeyPath(keyPath) - while keys.length > 1 - key = keys.shift() - object[key] ?= {} - object = object[key] - if value? - object[keys.shift()] = value - else - delete object[keys.shift()] - - hasKeyPath: (object, keyPath) -> - keys = splitKeyPath(keyPath) - for key in keys - return false unless object.hasOwnProperty(key) - object = object[key] - true - - spliceWithArray: (originalArray, start, length, insertedArray, chunkSize=100000) -> - if insertedArray.length < chunkSize - originalArray.splice(start, length, insertedArray...) - else - originalArray.splice(start, length) - for chunkStart in [0..insertedArray.length] by chunkSize - originalArray.splice(start + chunkStart, 0, insertedArray.slice(chunkStart, chunkStart + chunkSize)...) - - sum: (array) -> - sum = 0 - sum += elt for elt in array - sum - - uncamelcase: (string) -> - return '' unless string - - result = string.replace /([A-Z])|_+/g, (match, letter='') -> " #{letter}" - plus.capitalize(result.trim()) - - undasherize: (string) -> - if string - string.split('-').map(plus.capitalize).join(' ') - else - '' - - underscore: (string) -> - return '' unless string - - string = string[0].toLowerCase() + string[1..] - string.replace /([A-Z])|-+/g, (match, letter='') -> "_#{letter.toLowerCase()}" - - valueForKeyPath: (object, keyPath) -> - keys = splitKeyPath(keyPath) - for key in keys - object = object[key] - return unless object? - object - - isEqual: (a, b, aStack, bStack) -> - if _.isArray(aStack) and _.isArray(bStack) - isEqual(a, b, aStack, bStack) - else - isEqual(a, b) - - isEqualForProperties: (a, b, properties...) -> - for property in properties - return false unless _.isEqual(a[property], b[property]) - true - -isEqual = (a, b, aStack=[], bStack=[]) -> - return _.isEqual(a, b) if a is b - return _.isEqual(a, b) if _.isFunction(a) or _.isFunction(b) - - stackIndex = aStack.length - while stackIndex-- - return bStack[stackIndex] is b if aStack[stackIndex] is a - aStack.push(a) - bStack.push(b) - - equal = false - if _.isFunction(a?.isEqual) - equal = a.isEqual(b, aStack, bStack) - else if _.isFunction(b?.isEqual) - equal = b.isEqual(a, bStack, aStack) - else if _.isArray(a) and _.isArray(b) and a.length is b.length - equal = true - for aElement, i in a - unless isEqual(aElement, b[i], aStack, bStack) - equal = false - break - else if _.isRegExp(a) and _.isRegExp(b) - equal = _.isEqual(a, b) - else if _.isElement(a) and _.isElement(b) - equal = a is b - else if _.isObject(a) and _.isObject(b) - aCtor = a.constructor - bCtor = b.constructor - aCtorValid = _.isFunction(aCtor) and aCtor instanceof aCtor - bCtorValid = _.isFunction(bCtor) and bCtor instanceof bCtor - if aCtor isnt bCtor and not (aCtorValid and bCtorValid) - equal = false - else - aKeyCount = 0 - equal = true - for key, aValue of a - continue unless _.has(a, key) - aKeyCount++ - unless _.has(b, key) and isEqual(aValue, b[key], aStack, bStack) - equal = false - break - if equal - bKeyCount = 0 - for key, bValue of b - bKeyCount++ if _.has(b, key) - equal = aKeyCount is bKeyCount - else - equal = _.isEqual(a, b) - - aStack.pop() - bStack.pop() - equal - -module.exports = _.extend({}, _, plus) diff --git a/src/underscore-plus.mjs b/src/underscore-plus.mjs new file mode 100644 index 0000000..edc73c6 --- /dev/null +++ b/src/underscore-plus.mjs @@ -0,0 +1,460 @@ +export * from 'underscore' + +// import * as _ from 'underscore' // if we needed all +import {every, flatten, has, include, isArray, isElement, isEqual as _isEqual, isFunction, isObject, isRegExp, uniq} from "underscore"; + +const macModifierKeyMap = { + cmd: '\u2318', + ctrl: '\u2303', + alt: '\u2325', + option: '\u2325', + shift: '\u21e7', + enter: '\u23ce', + left: '\u2190', + right: '\u2192', + up: '\u2191', + down: '\u2193' +}; + +const nonMacModifierKeyMap = { + cmd: 'Cmd', + ctrl: 'Ctrl', + alt: 'Alt', + option: 'Alt', + shift: 'Shift', + enter: 'Enter', + left: 'Left', + right: 'Right', + up: 'Up', + down: 'Down' +}; + +// Human key combos should always explicitly state the shift key. This map is a disambiguator. +// 'shift-version': 'no-shift-version' +const shiftKeyMap = { + '~': '`', + '_': '-', + '+': '=', + '|': '\\', + '{': '[', + '}': ']', + ':': ';', + '"': '\'', + '<': ',', + '>': '.', + '?': '/' +}; + +function splitKeyPath(keyPath) { + let startIndex = 0; + const keyPathArray = []; + if (keyPath == null) { return keyPathArray; } + for (let i = 0; i < keyPath.length; i++) { + const char = keyPath[i]; + if ((char === '.') && ((i === 0) || (keyPath[i-1] !== '\\'))) { + keyPathArray.push(keyPath.substring(startIndex, i)); + startIndex = i + 1; + } + } + keyPathArray.push(keyPath.substr(startIndex, keyPath.length)); + return keyPathArray; +} + +const isPlainObject = value => isObject(value) && !isArray(value); + +export function adviseBefore(object, methodName, advice) { + const original = object[methodName]; + return object[methodName] = function(...args) { + if (advice.apply(this, args) !== false) { + return original.apply(this, args); + } + }; +} + +export function camelize(string) { + if (string) { + return string.replace(/[_-]+(\w)/g, m => m[1].toUpperCase()); + } else { + return ''; + } +} + +export function capitalize(word) { + if (!word) { return ''; } + + if (word.toLowerCase() === 'github') { + return 'GitHub'; + } else { + return word[0].toUpperCase() + word.slice(1); + } +} + +export function compactObject(object) { + const newObject = {}; + for (let key in object) { + const value = object[key]; + if (value != null) { newObject[key] = value; } + } + return newObject; +} + +export function dasherize(string) { + if (!string) { return ''; } + + string = string[0].toLowerCase() + string.slice(1); + return string.replace(/([A-Z])|(_)/g, function(m, letter) { + if (letter) { + return "-" + letter.toLowerCase(); + } else { + return "-"; + } + }); +} + +// Deep clones the given JSON object. +// +// `object` - The JSON object to clone. +// +// Returns a deep clone of the JSON object. +export function deepClone(object) { + if (isArray(object)) { + return object.map(value => deepClone(value)); + } else if (isObject(object) && !isFunction(object)) { + return mapObject(object, (key, value) => [key, deepClone(value)]); + } else { + return object; + } +} + +export function deepExtend(target) { + let result = target; + let i = 0; + while (++i < arguments.length) { + const object = arguments[i]; + if (isPlainObject(result) && isPlainObject(object)) { + const keys = Object.keys(object) + for (let key of keys) { + result[key] = deepExtend(result[key], object[key]); + } + } else { + result = deepClone(object); + } + } + return result; +} + +export function deepContains(array, target) { + if (array == null) { return false; } + for (let object of array) { + if (_isEqual(object, target)) { return true; } + } + return false; +} + +export function endsWith(string, suffix) { + if (suffix == null) { suffix = ''; } + if (string) { + return string.indexOf(suffix, string.length - suffix.length) !== -1; + } else { + return false; + } +} + +export function escapeAttribute(string) { + if (string) { + return string.replace(/"/g, '"').replace(/\n/g, '').replace(/\\/g, '-'); + } else { + return ''; + } +} + +export function escapeRegExp(string) { + if (string) { + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } else { + return ''; + } +} + +export function humanizeEventName(eventName, eventDoc) { + const [namespace, event] = eventName.split(':'); + if (event == null) { return undasherize(namespace); } + + const namespaceDoc = undasherize(namespace); + if (eventDoc == null) { eventDoc = undasherize(event); } + + return `${namespaceDoc}: ${eventDoc}`; +} + +export function humanizeKey(key, platform=process.platform) { + if (!key) { return key; } + + const modifierKeyMap = platform === 'darwin' ? macModifierKeyMap : nonMacModifierKeyMap; + + if (modifierKeyMap[key]) { + return modifierKeyMap[key]; + } else if ((key.length === 1) && (shiftKeyMap[key] != null)) { + return [modifierKeyMap.shift, shiftKeyMap[key]]; + } else if ((key.length === 1) && (key === key.toUpperCase()) && (key.toUpperCase() !== key.toLowerCase())) { + return [modifierKeyMap.shift, key.toUpperCase()]; + } else if ((key.length === 1) || /f[0-9]{1,2}/.test(key)) { + return key.toUpperCase(); + } else { + if (platform === 'darwin') { + return key; + } else { + return capitalize(key); + } + } +} + +// Humanize the keystroke according to platform conventions. This method +// attempts to mirror the text the given keystroke would have if displayed in +// a system menu. +// +// keystroke - A String keystroke to humanize such as `ctrl-O`. +// platform - An optional String platform to humanize for (default: +// `process.platform`). +// +// Returns a humanized representation of the keystroke. +export function humanizeKeystroke(keystroke, platform=process.platform) { + if (!keystroke) { return keystroke; } + + const keystrokes = keystroke.split(' '); + const humanizedKeystrokes = []; + for (keystroke of keystrokes) { + let keys = []; + const splitKeystroke = keystroke.split('-'); + for (let index = 0; index < splitKeystroke.length; index++) { + // Check for consecutive dashes such as cmd-- + let key = splitKeystroke[index]; + if ((key === '') && (splitKeystroke[index-1] === '')) { key = '-'; } + if (key) { keys.push(humanizeKey(key, platform)); } + } + + keys = uniq(flatten(keys)); + if (platform === 'darwin') { + keys = keys.join(''); + } else { + keys = keys.join('+'); + } + humanizedKeystrokes.push(keys); + } + + return humanizedKeystrokes.join(' '); +} + +export function isSubset(potentialSubset, potentialSuperset) { + return every(potentialSubset, element => include(potentialSuperset, element)); +} + +export function losslessInvert(hash) { + const inverted = {}; + for (let key in hash) { + const value = hash[key]; + if (inverted[value] == null) { inverted[value] = []; } + inverted[value].push(key); + } + return inverted; +} + +// Transform the given object into another object. +// +// `object` - The object to transform. +// `iterator` - +// A function that takes `(key, value)` arguments and returns a +// `[key, value]` tuple. +// +// Returns a new object based with the key/values returned by the iterator. +export function mapObject(object, iterator) { + const newObject = {}; + const keys = Object.keys(object) + for (let key of keys) { + let value; + [key, value] = iterator(key, object[key]); + newObject[key] = value; + } + + return newObject; +} + +export function multiplyString(string, n) { + let finalString = ""; + let i = 0; + while (i < n) { + finalString += string; + i++; + } + return finalString; +} + +export function pluralize(count=0, singular, plural=singular+'s') { + if (count === 1) { + return `${count} ${singular}`; + } else { + return `${count} ${plural}`; + } +} + +export function remove(array, element) { + const index = array.indexOf(element); + if (index >= 0) { array.splice(index, 1); } + return array; +} + +export function setValueForKeyPath(object, keyPath, value) { + const keys = splitKeyPath(keyPath); + while (keys.length > 1) { + const key = keys.shift(); + if (object[key] == null) { object[key] = {}; } + object = object[key]; + } + if (value != null) { + object[keys.shift()] = value; + } else { + delete object[keys.shift()]; + } +} + +export function hasKeyPath(object, keyPath) { + const keys = splitKeyPath(keyPath); + for (let key of keys) { + if (!object.hasOwnProperty(key)) { return false; } + object = object[key]; + } + return true; +} + +export function spliceWithArray(originalArray, start, length, insertedArray, chunkSize=100000) { + if (insertedArray.length < chunkSize) { + originalArray.splice(start, length, ...insertedArray); + } else { + originalArray.splice(start, length); + for (let chunkStart = 0, end = insertedArray.length; chunkStart <= end; chunkStart += chunkSize) { + originalArray.splice(start + chunkStart, 0, ...insertedArray.slice(chunkStart, chunkStart + chunkSize)); + } + } +} + +export function sum(array) { + let sum = 0; + for (let elt of array) { sum += elt; } + return sum; +} + +export function uncamelcase(string) { + if (!string) { return ''; } + + const result = string.replace(/([A-Z])|_+/g, (match, letter='') => ` ${letter}`); + return capitalize(result.trim()); +} + +export function undasherize(string) { + if (string) { + return string.split('-').map(capitalize).join(' '); + } else { + return ''; + } +} + +export function underscore(string) { + if (!string) { return ''; } + + string = string[0].toLowerCase() + string.slice(1); + return string.replace(/([A-Z])|-+/g, (match, letter='') => `_${letter.toLowerCase()}`); +} + +export function valueForKeyPath(object, keyPath) { + const keys = splitKeyPath(keyPath); + for (let key of keys) { + object = object[key]; + if (object == null) { return; } + } + return object; +} + +export function isEqual(a, b, aStack, bStack) { + if (isArray(aStack) && isArray(bStack)) { + return isEqual_(a, b, aStack, bStack); + } else { + return isEqual_(a, b); + } +} + +export function isEqualForProperties(a, b, ...properties) { + // TODO is Array.from needed? + for (let property of Array.from(properties)) { + if (!_isEqual(a[property], b[property])) { return false; } + } + return true; +} + +function isEqual_(a, b, aStack=[], bStack=[]) { + if (a === b) { return _isEqual(a, b); } + if (isFunction(a) || isFunction(b)) { return _isEqual(a, b); } + + let stackIndex = aStack.length; + while (stackIndex--) { + if (aStack[stackIndex] === a) { return bStack[stackIndex] === b; } + } + aStack.push(a); + bStack.push(b); + + let equal = false; + if (isFunction(a?.isEqual)) { + equal = a.isEqual(b, aStack, bStack); + } else if (isFunction(b?.isEqual)) { + equal = b.isEqual(a, bStack, aStack); + } else if (isArray(a) && isArray(b) && (a.length === b.length)) { + equal = true; + for (let i = 0; i < a.length; i++) { + const aElement = a[i]; + if (!isEqual_(aElement, b[i], aStack, bStack)) { + equal = false; + break; + } + } + } else if (isRegExp(a) && isRegExp(b)) { + equal = _isEqual(a, b); + } else if (isElement(a) && isElement(b)) { + equal = a === b; + } else if (isObject(a) && isObject(b)) { + const aCtor = a.constructor; + const bCtor = b.constructor; + const aCtorValid = isFunction(aCtor) && aCtor instanceof aCtor; + const bCtorValid = isFunction(bCtor) && bCtor instanceof bCtor; + if ((aCtor !== bCtor) && !(aCtorValid && bCtorValid)) { + equal = false; + } else { + let key; + let aKeyCount = 0; + equal = true; + for (key in a) { + const aValue = a[key]; + if (!has(a, key)) { continue; } + aKeyCount++; + if (!has(b, key) || !isEqual_(aValue, b[key], aStack, bStack)) { + equal = false; + break; + } + } + if (equal) { + let bKeyCount = 0; + for (key in b) { + const bValue = b[key]; + if (has(b, key)) { bKeyCount++; } + } + equal = aKeyCount === bKeyCount; + } + } + } else { + equal = _isEqual(a, b); + } + + aStack.pop(); + bStack.pop(); + return equal; +} + +// TODO: Consider shorter variations of null checks: +// https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md#ds207-consider-shorter-variations-of-null-checks