|
| 1 | +(function (root, stringify) { |
| 2 | + /* istanbul ignore else */ |
| 3 | + if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { |
| 4 | + // Node. |
| 5 | + module.exports = stringify(); |
| 6 | + } else if (typeof define === 'function' && define.amd) { |
| 7 | + // AMD, registers as an anonymous module. |
| 8 | + define(function () { |
| 9 | + return stringify(); |
| 10 | + }); |
| 11 | + } else { |
| 12 | + // Browser global. |
| 13 | + root.javascriptStringify = stringify(); |
| 14 | + } |
| 15 | +})(this, function () { |
| 16 | + /** |
| 17 | + * JavaScript reserved word list. |
| 18 | + */ |
| 19 | + var RESERVED_WORDS = {}; |
| 20 | + |
| 21 | + /** |
| 22 | + * Map reserved words to the object. |
| 23 | + */ |
| 24 | + ( |
| 25 | + 'break else new var case finally return void catch for switch while ' + |
| 26 | + 'continue function this with default if throw delete in try ' + |
| 27 | + 'do instanceof typeof abstract enum int short boolean export ' + |
| 28 | + 'interface static byte extends long super char final native synchronized ' + |
| 29 | + 'class float package throws const goto private transient debugger ' + |
| 30 | + 'implements protected volatile double import public let yield' |
| 31 | + ).split(' ').map(function (key) { |
| 32 | + RESERVED_WORDS[key] = true; |
| 33 | + }); |
| 34 | + |
| 35 | + /** |
| 36 | + * Check if a variable name is valid. |
| 37 | + * |
| 38 | + * @param {String} name |
| 39 | + * @return {Boolean} |
| 40 | + */ |
| 41 | + var isValidVariableName = function (name) { |
| 42 | + return !RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name); |
| 43 | + }; |
| 44 | + |
| 45 | + /** |
| 46 | + * Return the global variable name. |
| 47 | + * |
| 48 | + * @return {String} |
| 49 | + */ |
| 50 | + var getGlobalVariable = function () { |
| 51 | + return 'global'; |
| 52 | + }; |
| 53 | + |
| 54 | + /* istanbul ignore next */ |
| 55 | + if (typeof window === 'object' && typeof window.document === 'object') { |
| 56 | + // Support browser environments. |
| 57 | + getGlobalVariable = function () { |
| 58 | + return 'window'; |
| 59 | + }; |
| 60 | + } |
| 61 | + |
| 62 | + /* istanbul ignore next */ |
| 63 | + if (typeof self === 'object' && typeof self.importScripts === 'function') { |
| 64 | + // Support web worker environments. |
| 65 | + getGlobalVariable = function () { |
| 66 | + return 'self'; |
| 67 | + }; |
| 68 | + } |
| 69 | + |
| 70 | + /** |
| 71 | + * Convert JavaScript objects into strings. |
| 72 | + * |
| 73 | + * @type {Object} |
| 74 | + */ |
| 75 | + var OBJECT_TYPES = { |
| 76 | + '[object Array]': function (array, indent, stringify) { |
| 77 | + // Map array values to their stringified values with correct indentation. |
| 78 | + var values = array.map(function (value) { |
| 79 | + return indent + stringify(value).split('\n').join('\n' + indent); |
| 80 | + }).join(indent ? ',\n' : ','); |
| 81 | + |
| 82 | + // Wrap the array in newlines if we have indentation set. |
| 83 | + if (indent && values) { |
| 84 | + return '[\n' + values + '\n]'; |
| 85 | + } |
| 86 | + |
| 87 | + return '[' + values + ']'; |
| 88 | + }, |
| 89 | + '[object Object]': function (object, indent, stringify) { |
| 90 | + // Iterate over object keys and concat string together. |
| 91 | + var values = Object.keys(object).reduce(function (values, key) { |
| 92 | + var value = stringify(object[key]); |
| 93 | + |
| 94 | + // Omit `undefined` object values. |
| 95 | + if (value === undefined) { |
| 96 | + return values; |
| 97 | + } |
| 98 | + |
| 99 | + // String format the key and value data. |
| 100 | + key = isValidVariableName(key) ? key : stringify(key); |
| 101 | + value = String(value).split('\n').join('\n' + indent); |
| 102 | + |
| 103 | + // Push the current object key and value into the values array. |
| 104 | + values.push(indent + key + ':' + (indent ? ' ' : '') + value); |
| 105 | + |
| 106 | + return values; |
| 107 | + }, []).join(indent ? ',\n' : ','); |
| 108 | + |
| 109 | + // Wrap the object in newlines if we have indentation set. |
| 110 | + if (indent && values) { |
| 111 | + return '{\n' + values + '\n}'; |
| 112 | + } |
| 113 | + |
| 114 | + return '{' + values + '}'; |
| 115 | + }, |
| 116 | + '[object Date]': function (date, indent, stringify) { |
| 117 | + return 'new Date(' + date.getTime() + ')'; |
| 118 | + }, |
| 119 | + '[object String]': function (string, indent, stringify) { |
| 120 | + return 'new String(' + stringify(string.toString()) + ')'; |
| 121 | + }, |
| 122 | + '[object Number]': function (number, indent, stringify) { |
| 123 | + return 'new Number(' + number + ')'; |
| 124 | + }, |
| 125 | + '[object Boolean]': function (boolean, indent, stringify) { |
| 126 | + return 'new Boolean(' + boolean + ')'; |
| 127 | + }, |
| 128 | + '[object RegExp]': String, |
| 129 | + '[object Function]': String, |
| 130 | + '[object global]': getGlobalVariable, |
| 131 | + '[object Window]': getGlobalVariable |
| 132 | + }; |
| 133 | + |
| 134 | + /** |
| 135 | + * Convert JavaScript primitives into strings. |
| 136 | + * |
| 137 | + * @type {Object} |
| 138 | + */ |
| 139 | + var PRIMITIVE_TYPES = { |
| 140 | + 'string': function (string) { |
| 141 | + return '\'' + string.replace('\'', '\\\'') + '\''; |
| 142 | + }, |
| 143 | + 'number': String, |
| 144 | + 'object': String, |
| 145 | + 'boolean': String, |
| 146 | + 'undefined': String |
| 147 | + }; |
| 148 | + |
| 149 | + /** |
| 150 | + * Convert any value to a string. |
| 151 | + * |
| 152 | + * @param {*} value |
| 153 | + * @param {String} indent |
| 154 | + * @param {Function} stringify |
| 155 | + * @return {String} |
| 156 | + */ |
| 157 | + var stringify = function (value, indent, stringify) { |
| 158 | + // Convert primitives into strings. |
| 159 | + if (Object(value) !== value) { |
| 160 | + return PRIMITIVE_TYPES[typeof value](value, indent, stringify); |
| 161 | + } |
| 162 | + |
| 163 | + // Use the internal object string to select stringification method. |
| 164 | + var toString = OBJECT_TYPES[Object.prototype.toString.call(value)]; |
| 165 | + |
| 166 | + // Convert objects into strings. |
| 167 | + return toString && toString(value, indent, stringify); |
| 168 | + }; |
| 169 | + |
| 170 | + /** |
| 171 | + * Stringify an object into the literal string. |
| 172 | + * |
| 173 | + * @param {Object} value |
| 174 | + * @param {Function} [replacer] |
| 175 | + * @param {(Number|String)} [space] |
| 176 | + * @return {String} |
| 177 | + */ |
| 178 | + return function (value, replacer, space) { |
| 179 | + // Convert the spaces into a string. |
| 180 | + if (typeof space !== 'string') { |
| 181 | + space = new Array(space === +space ? Math.max(0, ++space) : 0).join(' '); |
| 182 | + } |
| 183 | + |
| 184 | + /** |
| 185 | + * Handle recursion by checking if we've visited this node every iteration. |
| 186 | + * |
| 187 | + * @param {*} value |
| 188 | + * @param {Array} cache |
| 189 | + * @return {String} |
| 190 | + */ |
| 191 | + var recurse = function (value, cache, next) { |
| 192 | + // If we've already visited this node before, break the recursion. |
| 193 | + if (cache.indexOf(value) > -1) { |
| 194 | + return; |
| 195 | + } |
| 196 | + |
| 197 | + // Push the value into the values cache to avoid an infinite loop. |
| 198 | + cache.push(value); |
| 199 | + |
| 200 | + // Stringify the value and fallback to |
| 201 | + return next(value, space, function (value) { |
| 202 | + return recurse(value, cache.slice(), next); |
| 203 | + }); |
| 204 | + }; |
| 205 | + |
| 206 | + // If the user defined a replacer function, make the recursion function |
| 207 | + // a double step process - `replacer -> stringify -> replacer -> etc`. |
| 208 | + if (typeof replacer === 'function') { |
| 209 | + return recurse(value, [], function (value, space, next) { |
| 210 | + return replacer(value, space, function (value) { |
| 211 | + return stringify(value, space, next); |
| 212 | + }); |
| 213 | + }); |
| 214 | + } |
| 215 | + |
| 216 | + return recurse(value, [], stringify); |
| 217 | + }; |
| 218 | +}); |
0 commit comments