(function (root, stringify) { /* istanbul ignore else */ if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { // Node. module.exports = stringify(); } else if (typeof define === 'function' && define.amd) { // AMD, registers as an anonymous module. define(function () { return stringify(); }); } else { // Browser global. root.javascriptStringify = stringify(); } })(this, function () { /** * Match all characters that need to be escaped in a string. Modified from * source to match single quotes instead of double. * * Source: https://github.com/douglascrockford/JSON-js/blob/master/json2.js */ var ESCAPABLE = /[\\\'\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; /** * Map of characters to escape characters. */ var META_CHARS = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', "'": "\\'", '"': '\\"', '\\': '\\\\' }; /** * Escape any character into its literal JavaScript string. * * @param {string} char * @return {string} */ function escapeChar (char) { var meta = META_CHARS[char]; return meta || '\\u' + ('0000' + char.charCodeAt(0).toString(16)).slice(-4); }; /** * JavaScript reserved word list. */ var RESERVED_WORDS = {}; /** * Map reserved words to the object. */ ( 'break else new var case finally return void catch for switch while ' + 'continue function this with default if throw delete in try ' + 'do instanceof typeof abstract enum int short boolean export ' + 'interface static byte extends long super char final native synchronized ' + 'class float package throws const goto private transient debugger ' + 'implements protected volatile double import public let yield' ).split(' ').map(function (key) { RESERVED_WORDS[key] = true; }); /** * Test for valid JavaScript identifier. */ var IS_VALID_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/; /** * Check if a variable name is valid. * * @param {string} name * @return {boolean} */ function isValidVariableName (name) { return !RESERVED_WORDS[name] && IS_VALID_IDENTIFIER.test(name); } /** * Return the global variable name. * * @return {string} */ function toGlobalVariable (value) { return 'Function(' + stringify('return this;') + ')()'; } /** * Serialize the path to a string. * * @param {Array} path * @return {string} */ function toPath (path) { var result = ''; for (var i = 0; i < path.length; i++) { if (isValidVariableName(path[i])) { result += '.' + path[i]; } else { result += '[' + stringify(path[i]) + ']'; } } return result; } /** * Stringify an array of values. * * @param {Array} array * @param {string} indent * @param {Function} next * @return {string} */ function stringifyArray (array, indent, next) { // Map array values to their stringified values with correct indentation. var values = array.map(function (value, index) { var str = next(value, index); if (str === undefined) { return String(str); } return indent + str.split('\n').join('\n' + indent); }).join(indent ? ',\n' : ','); // Wrap the array in newlines if we have indentation set. if (indent && values) { return '[\n' + values + '\n]'; } return '[' + values + ']'; } /** * Stringify a map of values. * * @param {Object} object * @param {string} indent * @param {Function} next * @return {string} */ function stringifyObject (object, indent, next) { // Iterate over object keys and concat string together. var values = Object.keys(object).reduce(function (values, key) { var value = next(object[key], key); // Omit `undefined` object values. if (value === undefined) { return values; } // String format the key and value data. key = isValidVariableName(key) ? key : stringify(key); value = String(value).split('\n').join('\n' + indent); // Push the current object key and value into the values array. values.push(indent + key + ':' + (indent ? ' ' : '') + value); return values; }, []).join(indent ? ',\n' : ','); // Wrap the object in newlines if we have indentation set. if (indent && values) { return '{\n' + values + '\n}'; } return '{' + values + '}'; } /** * Convert JavaScript objects into strings. */ var OBJECT_TYPES = { '[object Array]': stringifyArray, '[object Object]': stringifyObject, '[object Error]': function (error) { return 'new Error(' + stringify(error.message) + ')'; }, '[object Date]': function (date) { return 'new Date(' + date.getTime() + ')'; }, '[object String]': function (string) { return 'new String(' + stringify(string.toString()) + ')'; }, '[object Number]': function (number) { return 'new Number(' + number + ')'; }, '[object Boolean]': function (boolean) { return 'new Boolean(' + boolean + ')'; }, '[object Uint8Array]': function (array, indent) { return 'new Uint8Array(' + stringifyArray(array) + ')'; }, '[object Set]': function (array, indent, next) { if (typeof Array.from === 'function') { return 'new Set(' + stringify(Array.from(array), indent, next) + ')'; } else return undefined; }, '[object Map]': function (array, indent, next) { if (typeof Array.from === 'function') { return 'new Map(' + stringify(Array.from(array), indent, next) + ')'; } else return undefined; }, '[object RegExp]': String, '[object Function]': String, '[object global]': toGlobalVariable, '[object Window]': toGlobalVariable }; /** * Convert JavaScript primitives into strings. */ var PRIMITIVE_TYPES = { 'string': function (string) { return "'" + string.replace(ESCAPABLE, escapeChar) + "'"; }, 'number': String, 'object': String, 'boolean': String, 'symbol': String, 'undefined': String }; /** * Convert any value to a string. * * @param {*} value * @param {string} indent * @param {Function} next * @return {string} */ function stringify (value, indent, next) { // Convert primitives into strings. if (Object(value) !== value) { return PRIMITIVE_TYPES[typeof value](value, indent, next); } // Handle buffer objects before recursing (node < 6 was an object, node >= 6 is a `Uint8Array`). if (typeof Buffer === 'function' && Buffer.isBuffer(value)) { return 'new Buffer(' + next(value.toString()) + ')'; } // Use the internal object string to select stringification method. var toString = OBJECT_TYPES[Object.prototype.toString.call(value)]; // Convert objects into strings. return toString ? toString(value, indent, next) : undefined; } /** * Stringify an object into the literal string. * * @param {*} value * @param {Function} [replacer] * @param {(number|string)} [space] * @param {Object} [options] * @return {string} */ return function (value, replacer, space, options) { options = options || {} // Convert the spaces into a string. if (typeof space !== 'string') { space = new Array(Math.max(0, space|0) + 1).join(' '); } var maxDepth = Number(options.maxDepth) || 100; var references = !!options.references; var skipUndefinedProperties = !!options.skipUndefinedProperties; var valueCount = Number(options.maxValues) || 100000; var path = []; var stack = []; var encountered = []; var paths = []; var restore = []; /** * Stringify the next value in the stack. * * @param {*} value * @param {string} key * @return {string} */ function next (value, key) { if (skipUndefinedProperties && value === undefined) { return undefined; } path.push(key); var result = recurse(value, stringify); path.pop(); return result; } /** * Handle recursion by checking if we've visited this node every iteration. * * @param {*} value * @param {Function} stringify * @return {string} */ var recurse = references ? function (value, stringify) { if (value && (typeof value === 'object' || typeof value === 'function')) { var seen = encountered.indexOf(value); // Track nodes to restore later. if (seen > -1) { restore.push(path.slice(), paths[seen]); return; } // Track encountered nodes. encountered.push(value); paths.push(path.slice()); } // Stop when we hit the max depth. if (path.length > maxDepth || valueCount-- <= 0) { return; } // Stringify the value and fallback to return stringify(value, space, next); } : function (value, stringify) { var seen = stack.indexOf(value); if (seen > -1 || path.length > maxDepth || valueCount-- <= 0) { return; } stack.push(value); var value = stringify(value, space, next); stack.pop(); return value; }; // If the user defined a replacer function, make the recursion function // a double step process - `recurse -> replacer -> stringify`. if (typeof replacer === 'function') { var before = recurse // Intertwine the replacer function with the regular recursion. recurse = function (value, stringify) { return before(value, function (value, space, next) { return replacer(value, space, function (value) { return stringify(value, space, next); }); }); }; } var result = recurse(value, stringify); // Attempt to restore circular references. if (restore.length) { var sep = space ? '\n' : ''; var assignment = space ? ' = ' : '='; var eol = ';' + sep; var before = space ? '(function () {' : '(function(){' var after = '}())' var results = ['var x' + assignment + result]; for (var i = 0; i < restore.length; i += 2) { results.push('x' + toPath(restore[i]) + assignment + 'x' + toPath(restore[i + 1])); } results.push('return x'); return before + sep + results.join(eol) + eol + after } return result; }; });