Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 73 additions & 91 deletions javascript-stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -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' : ',');
Expand All @@ -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, '');
}

/**
Expand Down Expand Up @@ -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
};
Expand Down
80 changes: 28 additions & 52 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.