diff --git a/javascript-stringify.js b/javascript-stringify.js index f888eee..920b0ab 100644 --- a/javascript-stringify.js +++ b/javascript-stringify.js @@ -81,27 +81,6 @@ return !RESERVED_WORDS.hasOwnProperty(name) && IS_VALID_IDENTIFIER.test(name); } - /** - * Check if a function is an ES6 generator function - * - * @param {Function} fn - * @return {boolean} - */ - function isGeneratorFunction (fn) { - return fn.constructor.name === 'GeneratorFunction'; - } - - /** - * Can be replaced with `str.startsWith(prefix)` if code is updated to ES6. - * - * @param {string} str - * @param {string} prefix - * @return {boolean} - */ - function stringStartsWith (str, prefix) { - return str.substring(0, prefix.length) === prefix; - } - /** * Can be replaced with `str.repeat(count)` if code is updated to ES6. * @@ -122,6 +101,48 @@ return 'Function(' + stringify('return this;') + ')()'; } + /** + * Check if a method definition. + * + * @param {Function} fn + * @return {boolean} + */ + function isMethodDefinition(fn) { + switch (fn.constructor.name) { + case 'AsyncFunction': + return !/^async +(?:\(|function |[^\(\)\s]+ +=>)/.test(fn.toString()); + case 'GeneratorFunction': + return !/^function +\*/.test(fn.toString()); + case 'AsyncGeneratorFunction': + return !/^async +function +\*/.test(fn.toString()); + case 'Function': + return !/^(?:\(|function |[^\(\)\s]+ +=>)/.test(fn.toString()); + default: + return false; + } + } + + /** + * Return a valid function prefix based on type. + * + * @param {Function} fn + * @return {boolean} + */ + function functionPrefix(fn, shorthand) { + switch (fn.constructor.name) { + case 'AsyncFunction': + return shorthand ? 'async ' : 'async function '; + case 'GeneratorFunction': + return shorthand ? '* ' : 'function* '; + case 'AsyncGeneratorFunction': + return shorthand ? 'async * ' : 'async function*'; + case 'Function': + return shorthand ? '' : 'function '; + default: + return ''; + } + } + /** * Serialize the path to a string. * @@ -181,59 +202,27 @@ function stringifyObject (object, indent, next) { // Iterate over object keys and concat string together. var values = Object.keys(object).reduce(function (values, key) { - var value; - var addKey = true; - - // Handle functions specially to detect method notation. - if (typeof object[key] === 'function') { - var fn = object[key]; - var fnString = fn.toString(); - var prefix = isGeneratorFunction(fn) ? '*' : ''; - - // Was this function defined with method notation? - if (fn.name === key && stringStartsWith(fnString, prefix + key + '(')) { - if (isValidVariableName(key)) { - // The function is already in valid method notation. - value = fnString; - } else { - // Reformat the opening of the function into valid method notation. - value = prefix + stringify(key) + fnString.substring(prefix.length + key.length); - } + var property = object[key]; - // Dedent the function, since it didn't come through regular stringification. - if (indent) { - value = dedentFunction(value); - } + // Support object method definitions. + if (typeof property === 'function' && property.name === key && isMethodDefinition(property)) { + values.push(indent + dedentFunction(property.toString()).split('\n').join('\n' + indent)); + return values; + } - // Method notation includes the key, so there's no need to add it again below. - addKey = false; - } else { - // Not defined with method notation; delegate to regular stringification. - value = next(fn, key); - } - } else { - // `object[key]` is not a function. - value = next(object[key], key); + var value = next(property, key); - // Omit `undefined` object values. - if (value === undefined) { - return values; - } + // Omit `undefined` object values. + if (value === undefined) { + return values; } // String format the value data. - value = String(value).split('\n').join('\n' + indent); + var k = isValidVariableName(key) ? key : stringify(key); + var v = String(value).split('\n').join('\n' + indent); - if (addKey) { - // String format the key data. - key = isValidVariableName(key) ? key : stringify(key); - - // Push the current object key and value into the values array. - values.push(indent + key + ':' + (indent ? ' ' : '') + value); - } else { - // Push just the value; this is a method and no key is needed. - values.push(indent + value); - } + // Push the current object key and value into the values array. + values.push(indent + k + ':' + (indent ? ' ' : '') + v); return values; }, []).join(indent ? ',\n' : ','); @@ -254,40 +243,31 @@ */ function dedentFunction (fnString) { var indentationRegExp = /\n */g; + var size = Infinity; var match; - // Find the minimum amount of indentation used in the function body. - var dedent = Infinity; while (match = indentationRegExp.exec(fnString)) { - dedent = Math.min(dedent, match[0].length - 1); + size = Math.min(size, match[0].length - 1); } - if (isFinite(dedent)) { - return fnString.split('\n' + stringRepeat(' ', dedent)).join('\n'); - } else { - // Function is a one-liner and needs no adjustment. - return fnString; + if (isFinite(size)) { + return fnString.split('\n' + stringRepeat(' ', size)).join('\n'); } + + return fnString; } /** - * Stringify a function. - * - * @param {Function} fn - * @return {string} + * Create a function stringify. */ - function stringifyFunction (fn, indent) { - var value = fn.toString(); - if (indent) { - value = dedentFunction(value); - } - var prefix = isGeneratorFunction(fn) ? '*' : ''; - if (fn.name && stringStartsWith(value, prefix + fn.name + '(')) { - // Method notation was used to define this function, but it was transplanted from another object. - // Convert to regular function notation. - value = 'function' + prefix + ' ' + value.substring(prefix.length); - } - return value; + function stringifyFunction (fn) { + var value = dedentFunction(fn.toString()); + + if (!isMethodDefinition(fn)) return value; + + var prefix = functionPrefix(fn); + var name = isValidVariableName(fn.name) ? fn.name : ''; + return prefix + name + value.replace(/^[^(]+/g, ''); } /** @@ -320,6 +300,8 @@ '[object RegExp]': String, '[object Function]': stringifyFunction, '[object GeneratorFunction]': stringifyFunction, + '[object AsyncFunction]': stringifyFunction, + '[object AsyncGeneratorFunction]': stringifyFunction, '[object global]': toGlobalVariable, '[object Window]': toGlobalVariable }; diff --git a/package-lock.json b/package-lock.json index 9844cb8..eaab686 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,9 +55,9 @@ } }, "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "chai": { @@ -81,9 +81,9 @@ "dev": true }, "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", "dev": true }, "concat-map": { @@ -117,9 +117,9 @@ "dev": true }, "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, "escape-string-regexp": { @@ -201,9 +201,9 @@ } }, "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, "handlebars": { @@ -375,39 +375,24 @@ } }, "mocha": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz", - "integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", "dev": true, "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", + "browser-stdout": "1.3.1", + "commander": "2.15.1", "debug": "3.1.0", - "diff": "3.3.1", + "diff": "3.5.0", "escape-string-regexp": "1.0.5", "glob": "7.1.2", - "growl": "1.10.3", + "growl": "1.10.5", "he": "1.1.1", + "minimatch": "3.0.4", "mkdirp": "0.5.1", - "supports-color": "4.4.0" + "supports-color": "5.4.0" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -423,27 +408,18 @@ } }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "^2.0.0" + "has-flag": "^3.0.0" } } }