index.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /*
  2. Copyright (c) 2014, Yahoo! Inc. All rights reserved.
  3. Copyrights licensed under the New BSD License.
  4. See the accompanying LICENSE file for terms.
  5. */
  6. 'use strict';
  7. // Generate an internal UID to make the regexp pattern harder to guess.
  8. var UID = Math.floor(Math.random() * 0x10000000000).toString(16);
  9. var PLACE_HOLDER_REGEXP = new RegExp('"@__(F|R|D)-' + UID + '-(\\d+)__@"', 'g');
  10. var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
  11. var IS_PURE_FUNCTION = /function.*?\(/;
  12. var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
  13. var RESERVED_SYMBOLS = ['*', 'async'];
  14. // Mapping of unsafe HTML and invalid JavaScript line terminator chars to their
  15. // Unicode char counterparts which are safe to use in JavaScript strings.
  16. var ESCAPED_CHARS = {
  17. '<' : '\\u003C',
  18. '>' : '\\u003E',
  19. '/' : '\\u002F',
  20. '\u2028': '\\u2028',
  21. '\u2029': '\\u2029'
  22. };
  23. function escapeUnsafeChars(unsafeChar) {
  24. return ESCAPED_CHARS[unsafeChar];
  25. }
  26. module.exports = function serialize(obj, options) {
  27. options || (options = {});
  28. // Backwards-compatibility for `space` as the second argument.
  29. if (typeof options === 'number' || typeof options === 'string') {
  30. options = {space: options};
  31. }
  32. var functions = [];
  33. var regexps = [];
  34. var dates = [];
  35. // Returns placeholders for functions and regexps (identified by index)
  36. // which are later replaced by their string representation.
  37. function replacer(key, value) {
  38. if (!value) {
  39. return value;
  40. }
  41. // If the value is an object w/ a toJSON method, toJSON is called before
  42. // the replacer runs, so we use this[key] to get the non-toJSONed value.
  43. var origValue = this[key];
  44. var type = typeof origValue;
  45. if (type === 'object') {
  46. if(origValue instanceof RegExp) {
  47. return '@__R-' + UID + '-' + (regexps.push(origValue) - 1) + '__@';
  48. }
  49. if(origValue instanceof Date) {
  50. return '@__D-' + UID + '-' + (dates.push(origValue) - 1) + '__@';
  51. }
  52. }
  53. if (type === 'function') {
  54. return '@__F-' + UID + '-' + (functions.push(origValue) - 1) + '__@';
  55. }
  56. return value;
  57. }
  58. function serializeFunc(fn) {
  59. var serializedFn = fn.toString();
  60. if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) {
  61. throw new TypeError('Serializing native function: ' + fn.name);
  62. }
  63. // pure functions, example: {key: function() {}}
  64. if(IS_PURE_FUNCTION.test(serializedFn)) {
  65. return serializedFn;
  66. }
  67. var argsStartsAt = serializedFn.indexOf('(');
  68. var def = serializedFn.substr(0, argsStartsAt)
  69. .trim()
  70. .split(' ')
  71. .filter(function(val) { return val.length > 0 });
  72. var nonReservedSymbols = def.filter(function(val) {
  73. return RESERVED_SYMBOLS.indexOf(val) === -1
  74. });
  75. // enhanced literal objects, example: {key() {}}
  76. if(nonReservedSymbols.length > 0) {
  77. return (def.indexOf('async') > -1 ? 'async ' : '') + 'function'
  78. + (def.join('').indexOf('*') > -1 ? '*' : '')
  79. + serializedFn.substr(argsStartsAt);
  80. }
  81. // arrow functions
  82. return serializedFn;
  83. }
  84. var str;
  85. // Creates a JSON string representation of the value.
  86. // NOTE: Node 0.12 goes into slow mode with extra JSON.stringify() args.
  87. if (options.isJSON && !options.space) {
  88. str = JSON.stringify(obj);
  89. } else {
  90. str = JSON.stringify(obj, options.isJSON ? null : replacer, options.space);
  91. }
  92. // Protects against `JSON.stringify()` returning `undefined`, by serializing
  93. // to the literal string: "undefined".
  94. if (typeof str !== 'string') {
  95. return String(str);
  96. }
  97. // Replace unsafe HTML and invalid JavaScript line terminator chars with
  98. // their safe Unicode char counterpart. This _must_ happen before the
  99. // regexps and functions are serialized and added back to the string.
  100. if (options.unsafe !== true) {
  101. str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars);
  102. }
  103. if (functions.length === 0 && regexps.length === 0 && dates.length === 0) {
  104. return str;
  105. }
  106. // Replaces all occurrences of function, regexp and date placeholders in the
  107. // JSON string with their string representations. If the original value can
  108. // not be found, then `undefined` is used.
  109. return str.replace(PLACE_HOLDER_REGEXP, function (match, type, valueIndex) {
  110. if (type === 'D') {
  111. return "new Date(\"" + dates[valueIndex].toISOString() + "\")";
  112. }
  113. if (type === 'R') {
  114. return regexps[valueIndex].toString();
  115. }
  116. var fn = functions[valueIndex];
  117. return serializeFunc(fn);
  118. });
  119. }