You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
5.5 KiB
173 lines
5.5 KiB
/* |
|
Copyright (c) 2014, Yahoo! Inc. All rights reserved. |
|
Copyrights licensed under the New BSD License. |
|
See the accompanying LICENSE file for terms. |
|
*/ |
|
|
|
'use strict'; |
|
|
|
// Generate an internal UID to make the regexp pattern harder to guess. |
|
var UID = Math.floor(Math.random() * 0x10000000000).toString(16); |
|
var PLACE_HOLDER_REGEXP = new RegExp('"@__(F|R|D|M|S)-' + UID + '-(\\d+)__@"', 'g'); |
|
|
|
var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g; |
|
var IS_PURE_FUNCTION = /function.*?\(/; |
|
var IS_ARROW_FUNCTION = /.*?=>.*?/; |
|
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g; |
|
|
|
var RESERVED_SYMBOLS = ['*', 'async']; |
|
|
|
// Mapping of unsafe HTML and invalid JavaScript line terminator chars to their |
|
// Unicode char counterparts which are safe to use in JavaScript strings. |
|
var ESCAPED_CHARS = { |
|
'<' : '\\u003C', |
|
'>' : '\\u003E', |
|
'/' : '\\u002F', |
|
'\u2028': '\\u2028', |
|
'\u2029': '\\u2029' |
|
}; |
|
|
|
function escapeUnsafeChars(unsafeChar) { |
|
return ESCAPED_CHARS[unsafeChar]; |
|
} |
|
|
|
module.exports = function serialize(obj, options) { |
|
options || (options = {}); |
|
|
|
// Backwards-compatibility for `space` as the second argument. |
|
if (typeof options === 'number' || typeof options === 'string') { |
|
options = {space: options}; |
|
} |
|
|
|
var functions = []; |
|
var regexps = []; |
|
var dates = []; |
|
var maps = []; |
|
var sets = []; |
|
|
|
// Returns placeholders for functions and regexps (identified by index) |
|
// which are later replaced by their string representation. |
|
function replacer(key, value) { |
|
if (!value) { |
|
return value; |
|
} |
|
|
|
// If the value is an object w/ a toJSON method, toJSON is called before |
|
// the replacer runs, so we use this[key] to get the non-toJSONed value. |
|
var origValue = this[key]; |
|
var type = typeof origValue; |
|
|
|
if (type === 'object') { |
|
if(origValue instanceof RegExp) { |
|
return '@__R-' + UID + '-' + (regexps.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if(origValue instanceof Date) { |
|
return '@__D-' + UID + '-' + (dates.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if(origValue instanceof Map) { |
|
return '@__M-' + UID + '-' + (maps.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if(origValue instanceof Set) { |
|
return '@__S-' + UID + '-' + (sets.push(origValue) - 1) + '__@'; |
|
} |
|
} |
|
|
|
if (type === 'function') { |
|
return '@__F-' + UID + '-' + (functions.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
return value; |
|
} |
|
|
|
function serializeFunc(fn) { |
|
var serializedFn = fn.toString(); |
|
if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) { |
|
throw new TypeError('Serializing native function: ' + fn.name); |
|
} |
|
|
|
// pure functions, example: {key: function() {}} |
|
if(IS_PURE_FUNCTION.test(serializedFn)) { |
|
return serializedFn; |
|
} |
|
|
|
// arrow functions, example: arg1 => arg1+5 |
|
if(IS_ARROW_FUNCTION.test(serializedFn)) { |
|
return serializedFn; |
|
} |
|
|
|
var argsStartsAt = serializedFn.indexOf('('); |
|
var def = serializedFn.substr(0, argsStartsAt) |
|
.trim() |
|
.split(' ') |
|
.filter(function(val) { return val.length > 0 }); |
|
|
|
var nonReservedSymbols = def.filter(function(val) { |
|
return RESERVED_SYMBOLS.indexOf(val) === -1 |
|
}); |
|
|
|
// enhanced literal objects, example: {key() {}} |
|
if(nonReservedSymbols.length > 0) { |
|
return (def.indexOf('async') > -1 ? 'async ' : '') + 'function' |
|
+ (def.join('').indexOf('*') > -1 ? '*' : '') |
|
+ serializedFn.substr(argsStartsAt); |
|
} |
|
|
|
// arrow functions |
|
return serializedFn; |
|
} |
|
|
|
var str; |
|
|
|
// Creates a JSON string representation of the value. |
|
// NOTE: Node 0.12 goes into slow mode with extra JSON.stringify() args. |
|
if (options.isJSON && !options.space) { |
|
str = JSON.stringify(obj); |
|
} else { |
|
str = JSON.stringify(obj, options.isJSON ? null : replacer, options.space); |
|
} |
|
|
|
// Protects against `JSON.stringify()` returning `undefined`, by serializing |
|
// to the literal string: "undefined". |
|
if (typeof str !== 'string') { |
|
return String(str); |
|
} |
|
|
|
// Replace unsafe HTML and invalid JavaScript line terminator chars with |
|
// their safe Unicode char counterpart. This _must_ happen before the |
|
// regexps and functions are serialized and added back to the string. |
|
if (options.unsafe !== true) { |
|
str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars); |
|
} |
|
|
|
if (functions.length === 0 && regexps.length === 0 && dates.length === 0 && maps.length === 0 && sets.length === 0) { |
|
return str; |
|
} |
|
|
|
// Replaces all occurrences of function, regexp, date, map and set placeholders in the |
|
// JSON string with their string representations. If the original value can |
|
// not be found, then `undefined` is used. |
|
return str.replace(PLACE_HOLDER_REGEXP, function (match, type, valueIndex) { |
|
if (type === 'D') { |
|
return "new Date(\"" + dates[valueIndex].toISOString() + "\")"; |
|
} |
|
|
|
if (type === 'R') { |
|
return regexps[valueIndex].toString(); |
|
} |
|
|
|
if (type === 'M') { |
|
return "new Map(" + serialize(Array.from(maps[valueIndex].entries()), options) + ")"; |
|
} |
|
|
|
if (type === 'S') { |
|
return "new Set(" + serialize(Array.from(sets[valueIndex].values()), options) + ")"; |
|
} |
|
|
|
var fn = functions[valueIndex]; |
|
|
|
return serializeFunc(fn); |
|
}); |
|
}
|
|
|