javascript-stringify.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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. * Match all characters that need to be escaped in a string. Modified from
  18. * source to match single quotes instead of double.
  19. *
  20. * Source: https://github.com/douglascrockford/JSON-js/blob/master/json2.js
  21. */
  22. var ESCAPABLE = /[\\\'\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
  23. /**
  24. * Map of characters to escape characters.
  25. */
  26. var META_CHARS = {
  27. '\b': '\\b',
  28. '\t': '\\t',
  29. '\n': '\\n',
  30. '\f': '\\f',
  31. '\r': '\\r',
  32. "'": "\\'",
  33. '"': '\\"',
  34. '\\': '\\\\'
  35. };
  36. /**
  37. * Escape any character into its literal JavaScript string.
  38. *
  39. * @param {string} char
  40. * @return {string}
  41. */
  42. function escapeChar (char) {
  43. var meta = META_CHARS[char];
  44. return meta || '\\u' + ('0000' + char.charCodeAt(0).toString(16)).slice(-4);
  45. };
  46. /**
  47. * JavaScript reserved word list.
  48. */
  49. var RESERVED_WORDS = {};
  50. /**
  51. * Map reserved words to the object.
  52. */
  53. (
  54. 'break else new var case finally return void catch for switch while ' +
  55. 'continue function this with default if throw delete in try ' +
  56. 'do instanceof typeof abstract enum int short boolean export ' +
  57. 'interface static byte extends long super char final native synchronized ' +
  58. 'class float package throws const goto private transient debugger ' +
  59. 'implements protected volatile double import public let yield'
  60. ).split(' ').map(function (key) {
  61. RESERVED_WORDS[key] = true;
  62. });
  63. /**
  64. * Test for valid JavaScript identifier.
  65. */
  66. var IS_VALID_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
  67. /**
  68. * Check if a variable name is valid.
  69. *
  70. * @param {string} name
  71. * @return {boolean}
  72. */
  73. function isValidVariableName (name) {
  74. return !RESERVED_WORDS[name] && IS_VALID_IDENTIFIER.test(name);
  75. }
  76. /**
  77. * Return the global variable name.
  78. *
  79. * @return {string}
  80. */
  81. function toGlobalVariable (value) {
  82. return 'Function(' + stringify('return this;') + ')()';
  83. }
  84. /**
  85. * Serialize the path to a string.
  86. *
  87. * @param {Array} path
  88. * @return {string}
  89. */
  90. function toPath (path) {
  91. var result = '';
  92. for (var i = 0; i < path.length; i++) {
  93. if (isValidVariableName(path[i])) {
  94. result += '.' + path[i];
  95. } else {
  96. result += '[' + stringify(path[i]) + ']';
  97. }
  98. }
  99. return result;
  100. }
  101. /**
  102. * Stringify an array of values.
  103. *
  104. * @param {Array} array
  105. * @param {string} indent
  106. * @param {Function} next
  107. * @return {string}
  108. */
  109. function stringifyArray (array, indent, next) {
  110. // Map array values to their stringified values with correct indentation.
  111. var values = array.map(function (value, index) {
  112. var str = next(value, index);
  113. if (str === undefined) {
  114. return String(str);
  115. }
  116. return indent + str.split('\n').join('\n' + indent);
  117. }).join(indent ? ',\n' : ',');
  118. // Wrap the array in newlines if we have indentation set.
  119. if (indent && values) {
  120. return '[\n' + values + '\n]';
  121. }
  122. return '[' + values + ']';
  123. }
  124. /**
  125. * Stringify a map of values.
  126. *
  127. * @param {Object} object
  128. * @param {string} indent
  129. * @param {Function} next
  130. * @return {string}
  131. */
  132. function stringifyObject (object, indent, next) {
  133. // Iterate over object keys and concat string together.
  134. var values = Object.keys(object).reduce(function (values, key) {
  135. var value = next(object[key], key);
  136. // Omit `undefined` object values.
  137. if (value === undefined) {
  138. return values;
  139. }
  140. // String format the key and value data.
  141. key = isValidVariableName(key) ? key : stringify(key);
  142. value = String(value).split('\n').join('\n' + indent);
  143. // Push the current object key and value into the values array.
  144. values.push(indent + key + ':' + (indent ? ' ' : '') + value);
  145. return values;
  146. }, []).join(indent ? ',\n' : ',');
  147. // Wrap the object in newlines if we have indentation set.
  148. if (indent && values) {
  149. return '{\n' + values + '\n}';
  150. }
  151. return '{' + values + '}';
  152. }
  153. /**
  154. * Convert JavaScript objects into strings.
  155. */
  156. var OBJECT_TYPES = {
  157. '[object Array]': stringifyArray,
  158. '[object Object]': stringifyObject,
  159. '[object Error]': function (error) {
  160. return 'new Error(' + stringify(error.message) + ')';
  161. },
  162. '[object Date]': function (date) {
  163. return 'new Date(' + date.getTime() + ')';
  164. },
  165. '[object String]': function (string) {
  166. return 'new String(' + stringify(string.toString()) + ')';
  167. },
  168. '[object Number]': function (number) {
  169. return 'new Number(' + number + ')';
  170. },
  171. '[object Boolean]': function (boolean) {
  172. return 'new Boolean(' + boolean + ')';
  173. },
  174. '[object Uint8Array]': function (array, indent) {
  175. return 'new Uint8Array(' + stringifyArray(array) + ')';
  176. },
  177. '[object Set]': function (array, indent, next) {
  178. if (typeof Array.from === 'function') {
  179. return 'new Set(' + stringify(Array.from(array), indent, next) + ')';
  180. } else return undefined;
  181. },
  182. '[object Map]': function (array, indent, next) {
  183. if (typeof Array.from === 'function') {
  184. return 'new Map(' + stringify(Array.from(array), indent, next) + ')';
  185. } else return undefined;
  186. },
  187. '[object RegExp]': String,
  188. '[object Function]': String,
  189. '[object global]': toGlobalVariable,
  190. '[object Window]': toGlobalVariable
  191. };
  192. /**
  193. * Convert JavaScript primitives into strings.
  194. */
  195. var PRIMITIVE_TYPES = {
  196. 'string': function (string) {
  197. return "'" + string.replace(ESCAPABLE, escapeChar) + "'";
  198. },
  199. 'number': String,
  200. 'object': String,
  201. 'boolean': String,
  202. 'symbol': String,
  203. 'undefined': String
  204. };
  205. /**
  206. * Convert any value to a string.
  207. *
  208. * @param {*} value
  209. * @param {string} indent
  210. * @param {Function} next
  211. * @return {string}
  212. */
  213. function stringify (value, indent, next) {
  214. // Convert primitives into strings.
  215. if (Object(value) !== value) {
  216. return PRIMITIVE_TYPES[typeof value](value, indent, next);
  217. }
  218. // Handle buffer objects before recursing (node < 6 was an object, node >= 6 is a `Uint8Array`).
  219. if (typeof Buffer === 'function' && Buffer.isBuffer(value)) {
  220. return 'new Buffer(' + next(value.toString()) + ')';
  221. }
  222. // Use the internal object string to select stringification method.
  223. var toString = OBJECT_TYPES[Object.prototype.toString.call(value)];
  224. // Convert objects into strings.
  225. return toString ? toString(value, indent, next) : undefined;
  226. }
  227. /**
  228. * Stringify an object into the literal string.
  229. *
  230. * @param {*} value
  231. * @param {Function} [replacer]
  232. * @param {(number|string)} [space]
  233. * @param {Object} [options]
  234. * @return {string}
  235. */
  236. return function (value, replacer, space, options) {
  237. options = options || {}
  238. // Convert the spaces into a string.
  239. if (typeof space !== 'string') {
  240. space = new Array(Math.max(0, space|0) + 1).join(' ');
  241. }
  242. var maxDepth = Number(options.maxDepth) || 100;
  243. var references = !!options.references;
  244. var skipUndefinedProperties = !!options.skipUndefinedProperties;
  245. var valueCount = Number(options.maxValues) || 100000;
  246. var path = [];
  247. var stack = [];
  248. var encountered = [];
  249. var paths = [];
  250. var restore = [];
  251. /**
  252. * Stringify the next value in the stack.
  253. *
  254. * @param {*} value
  255. * @param {string} key
  256. * @return {string}
  257. */
  258. function next (value, key) {
  259. if (skipUndefinedProperties && value === undefined) {
  260. return undefined;
  261. }
  262. path.push(key);
  263. var result = recurse(value, stringify);
  264. path.pop();
  265. return result;
  266. }
  267. /**
  268. * Handle recursion by checking if we've visited this node every iteration.
  269. *
  270. * @param {*} value
  271. * @param {Function} stringify
  272. * @return {string}
  273. */
  274. var recurse = references ?
  275. function (value, stringify) {
  276. if (value && (typeof value === 'object' || typeof value === 'function')) {
  277. var seen = encountered.indexOf(value);
  278. // Track nodes to restore later.
  279. if (seen > -1) {
  280. restore.push(path.slice(), paths[seen]);
  281. return;
  282. }
  283. // Track encountered nodes.
  284. encountered.push(value);
  285. paths.push(path.slice());
  286. }
  287. // Stop when we hit the max depth.
  288. if (path.length > maxDepth || valueCount-- <= 0) {
  289. return;
  290. }
  291. // Stringify the value and fallback to
  292. return stringify(value, space, next);
  293. } :
  294. function (value, stringify) {
  295. var seen = stack.indexOf(value);
  296. if (seen > -1 || path.length > maxDepth || valueCount-- <= 0) {
  297. return;
  298. }
  299. stack.push(value);
  300. var value = stringify(value, space, next);
  301. stack.pop();
  302. return value;
  303. };
  304. // If the user defined a replacer function, make the recursion function
  305. // a double step process - `recurse -> replacer -> stringify`.
  306. if (typeof replacer === 'function') {
  307. var before = recurse
  308. // Intertwine the replacer function with the regular recursion.
  309. recurse = function (value, stringify) {
  310. return before(value, function (value, space, next) {
  311. return replacer(value, space, function (value) {
  312. return stringify(value, space, next);
  313. });
  314. });
  315. };
  316. }
  317. var result = recurse(value, stringify);
  318. // Attempt to restore circular references.
  319. if (restore.length) {
  320. var sep = space ? '\n' : '';
  321. var assignment = space ? ' = ' : '=';
  322. var eol = ';' + sep;
  323. var before = space ? '(function () {' : '(function(){'
  324. var after = '}())'
  325. var results = ['var x' + assignment + result];
  326. for (var i = 0; i < restore.length; i += 2) {
  327. results.push('x' + toPath(restore[i]) + assignment + 'x' + toPath(restore[i + 1]));
  328. }
  329. results.push('return x');
  330. return before + sep + results.join(eol) + eol + after
  331. }
  332. return result;
  333. };
  334. });