|
81 | 81 | return !RESERVED_WORDS.hasOwnProperty(name) && IS_VALID_IDENTIFIER.test(name); |
82 | 82 | } |
83 | 83 |
|
| 84 | + /** |
| 85 | + * Check if a function is an ES6 generator function |
| 86 | + * |
| 87 | + * @param {Function} fn |
| 88 | + * @return {boolean} |
| 89 | + */ |
| 90 | + function isGeneratorFunction (fn) { |
| 91 | + return fn.constructor.name === 'GeneratorFunction'; |
| 92 | + } |
| 93 | + |
| 94 | + /** |
| 95 | + * Can be replaced with `str.startsWith(prefix)` if code is updated to ES6. |
| 96 | + * |
| 97 | + * @param {string} str |
| 98 | + * @param {string} prefix |
| 99 | + * @return {boolean} |
| 100 | + */ |
| 101 | + function stringStartsWith (str, prefix) { |
| 102 | + return str.substring(0, prefix.length) === prefix; |
| 103 | + } |
| 104 | + |
| 105 | + /** |
| 106 | + * Can be replaced with `str.repeat(count)` if code is updated to ES6. |
| 107 | + * |
| 108 | + * @param {string} str |
| 109 | + * @param {number} count |
| 110 | + * @return {string} |
| 111 | + */ |
| 112 | + function stringRepeat (str, count) { |
| 113 | + return new Array(Math.max(0, count|0) + 1).join(str); |
| 114 | + } |
| 115 | + |
84 | 116 | /** |
85 | 117 | * Return the global variable name. |
86 | 118 | * |
|
149 | 181 | function stringifyObject (object, indent, next) { |
150 | 182 | // Iterate over object keys and concat string together. |
151 | 183 | var values = Object.keys(object).reduce(function (values, key) { |
152 | | - var value = next(object[key], key); |
| 184 | + var value; |
| 185 | + var addKey = true; |
| 186 | + |
| 187 | + // Handle functions specially to detect method notation. |
| 188 | + if (typeof object[key] === 'function') { |
| 189 | + var fn = object[key]; |
| 190 | + var fnString = fn.toString(); |
| 191 | + var prefix = isGeneratorFunction(fn) ? '*' : ''; |
| 192 | + |
| 193 | + // Was this function defined with method notation? |
| 194 | + if (fn.name === key && stringStartsWith(fnString, prefix + key + '(')) { |
| 195 | + if (isValidVariableName(key)) { |
| 196 | + // The function is already in valid method notation. |
| 197 | + value = fnString; |
| 198 | + } else { |
| 199 | + // Reformat the opening of the function into valid method notation. |
| 200 | + value = prefix + stringify(key) + fnString.substring(prefix.length + key.length); |
| 201 | + } |
| 202 | + |
| 203 | + // Dedent the function, since it didn't come through regular stringification. |
| 204 | + if (indent) { |
| 205 | + value = dedentFunction(value); |
| 206 | + } |
153 | 207 |
|
154 | | - // Omit `undefined` object values. |
155 | | - if (value === undefined) { |
156 | | - return values; |
| 208 | + // Method notation includes the key, so there's no need to add it again below. |
| 209 | + addKey = false; |
| 210 | + } else { |
| 211 | + // Not defined with method notation; delegate to regular stringification. |
| 212 | + value = next(fn, key); |
| 213 | + } |
| 214 | + } else { |
| 215 | + // `object[key]` is not a function. |
| 216 | + value = next(object[key], key); |
| 217 | + |
| 218 | + // Omit `undefined` object values. |
| 219 | + if (value === undefined) { |
| 220 | + return values; |
| 221 | + } |
157 | 222 | } |
158 | 223 |
|
159 | | - // String format the key and value data. |
160 | | - key = isValidVariableName(key) ? key : stringify(key); |
| 224 | + // String format the value data. |
161 | 225 | value = String(value).split('\n').join('\n' + indent); |
162 | 226 |
|
163 | | - // Push the current object key and value into the values array. |
164 | | - values.push(indent + key + ':' + (indent ? ' ' : '') + value); |
| 227 | + if (addKey) { |
| 228 | + // String format the key data. |
| 229 | + key = isValidVariableName(key) ? key : stringify(key); |
| 230 | + |
| 231 | + // Push the current object key and value into the values array. |
| 232 | + values.push(indent + key + ':' + (indent ? ' ' : '') + value); |
| 233 | + } else { |
| 234 | + // Push just the value; this is a method and no key is needed. |
| 235 | + values.push(indent + value); |
| 236 | + } |
165 | 237 |
|
166 | 238 | return values; |
167 | 239 | }, []).join(indent ? ',\n' : ','); |
|
174 | 246 | return '{' + values + '}'; |
175 | 247 | } |
176 | 248 |
|
| 249 | + /** |
| 250 | + * Rewrite a stringified function to remove initial indentation. |
| 251 | + * |
| 252 | + * @param {string} fnString |
| 253 | + * @return {string} |
| 254 | + */ |
| 255 | + function dedentFunction (fnString) { |
| 256 | + var indentationRegExp = /\n */g; |
| 257 | + var match; |
| 258 | + |
| 259 | + // Find the minimum amount of indentation used in the function body. |
| 260 | + var dedent = Infinity; |
| 261 | + while (match = indentationRegExp.exec(fnString)) { |
| 262 | + dedent = Math.min(dedent, match[0].length - 1); |
| 263 | + } |
| 264 | + |
| 265 | + if (isFinite(dedent)) { |
| 266 | + return fnString.split('\n' + stringRepeat(' ', dedent)).join('\n'); |
| 267 | + } else { |
| 268 | + // Function is a one-liner and needs no adjustment. |
| 269 | + return fnString; |
| 270 | + } |
| 271 | + } |
| 272 | + |
| 273 | + /** |
| 274 | + * Stringify a function. |
| 275 | + * |
| 276 | + * @param {Function} fn |
| 277 | + * @return {string} |
| 278 | + */ |
| 279 | + function stringifyFunction (fn, indent) { |
| 280 | + var value = fn.toString(); |
| 281 | + if (indent) { |
| 282 | + value = dedentFunction(value); |
| 283 | + } |
| 284 | + var prefix = isGeneratorFunction(fn) ? '*' : ''; |
| 285 | + if (fn.name && stringStartsWith(value, prefix + fn.name + '(')) { |
| 286 | + // Method notation was used to define this function, but it was transplanted from another object. |
| 287 | + // Convert to regular function notation. |
| 288 | + value = 'function' + prefix + ' ' + value.substring(prefix.length); |
| 289 | + } |
| 290 | + return value; |
| 291 | + } |
| 292 | + |
177 | 293 | /** |
178 | 294 | * Convert JavaScript objects into strings. |
179 | 295 | */ |
|
202 | 318 | return 'new Map(' + stringify(Array.from(array), indent, next) + ')'; |
203 | 319 | }, |
204 | 320 | '[object RegExp]': String, |
205 | | - '[object Function]': String, |
| 321 | + '[object Function]': stringifyFunction, |
| 322 | + '[object GeneratorFunction]': stringifyFunction, |
206 | 323 | '[object global]': toGlobalVariable, |
207 | 324 | '[object Window]': toGlobalVariable |
208 | 325 | }; |
|
263 | 380 |
|
264 | 381 | // Convert the spaces into a string. |
265 | 382 | if (typeof space !== 'string') { |
266 | | - space = new Array(Math.max(0, space|0) + 1).join(' '); |
| 383 | + space = stringRepeat(' ', space); |
267 | 384 | } |
268 | 385 |
|
269 | 386 | var maxDepth = Number(options.maxDepth) || 100; |
|
0 commit comments