stringify.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. 'use strict';
  2. var getSideChannel = require('side-channel');
  3. var utils = require('./utils');
  4. var formats = require('./formats');
  5. var has = Object.prototype.hasOwnProperty;
  6. var arrayPrefixGenerators = {
  7. brackets: function brackets(prefix) {
  8. return prefix + '[]';
  9. },
  10. comma: 'comma',
  11. indices: function indices(prefix, key) {
  12. return prefix + '[' + key + ']';
  13. },
  14. repeat: function repeat(prefix) {
  15. return prefix;
  16. }
  17. };
  18. var isArray = Array.isArray;
  19. var push = Array.prototype.push;
  20. var pushToArray = function (arr, valueOrArray) {
  21. push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]);
  22. };
  23. var toISO = Date.prototype.toISOString;
  24. var defaultFormat = formats['default'];
  25. var defaults = {
  26. addQueryPrefix: false,
  27. allowDots: false,
  28. allowEmptyArrays: false,
  29. arrayFormat: 'indices',
  30. charset: 'utf-8',
  31. charsetSentinel: false,
  32. commaRoundTrip: false,
  33. delimiter: '&',
  34. encode: true,
  35. encodeDotInKeys: false,
  36. encoder: utils.encode,
  37. encodeValuesOnly: false,
  38. filter: void undefined,
  39. format: defaultFormat,
  40. formatter: formats.formatters[defaultFormat],
  41. // deprecated
  42. indices: false,
  43. serializeDate: function serializeDate(date) {
  44. return toISO.call(date);
  45. },
  46. skipNulls: false,
  47. strictNullHandling: false
  48. };
  49. var isNonNullishPrimitive = function isNonNullishPrimitive(v) {
  50. return typeof v === 'string'
  51. || typeof v === 'number'
  52. || typeof v === 'boolean'
  53. || typeof v === 'symbol'
  54. || typeof v === 'bigint';
  55. };
  56. var sentinel = {};
  57. var stringify = function stringify(
  58. object,
  59. prefix,
  60. generateArrayPrefix,
  61. commaRoundTrip,
  62. allowEmptyArrays,
  63. strictNullHandling,
  64. skipNulls,
  65. encodeDotInKeys,
  66. encoder,
  67. filter,
  68. sort,
  69. allowDots,
  70. serializeDate,
  71. format,
  72. formatter,
  73. encodeValuesOnly,
  74. charset,
  75. sideChannel
  76. ) {
  77. var obj = object;
  78. var tmpSc = sideChannel;
  79. var step = 0;
  80. var findFlag = false;
  81. while ((tmpSc = tmpSc.get(sentinel)) !== void undefined && !findFlag) {
  82. // Where object last appeared in the ref tree
  83. var pos = tmpSc.get(object);
  84. step += 1;
  85. if (typeof pos !== 'undefined') {
  86. if (pos === step) {
  87. throw new RangeError('Cyclic object value');
  88. } else {
  89. findFlag = true; // Break while
  90. }
  91. }
  92. if (typeof tmpSc.get(sentinel) === 'undefined') {
  93. step = 0;
  94. }
  95. }
  96. if (typeof filter === 'function') {
  97. obj = filter(prefix, obj);
  98. } else if (obj instanceof Date) {
  99. obj = serializeDate(obj);
  100. } else if (generateArrayPrefix === 'comma' && isArray(obj)) {
  101. obj = utils.maybeMap(obj, function (value) {
  102. if (value instanceof Date) {
  103. return serializeDate(value);
  104. }
  105. return value;
  106. });
  107. }
  108. if (obj === null) {
  109. if (strictNullHandling) {
  110. return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key', format) : prefix;
  111. }
  112. obj = '';
  113. }
  114. if (isNonNullishPrimitive(obj) || utils.isBuffer(obj)) {
  115. if (encoder) {
  116. var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder, charset, 'key', format);
  117. return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder, charset, 'value', format))];
  118. }
  119. return [formatter(prefix) + '=' + formatter(String(obj))];
  120. }
  121. var values = [];
  122. if (typeof obj === 'undefined') {
  123. return values;
  124. }
  125. var objKeys;
  126. if (generateArrayPrefix === 'comma' && isArray(obj)) {
  127. // we need to join elements in
  128. if (encodeValuesOnly && encoder) {
  129. obj = utils.maybeMap(obj, encoder);
  130. }
  131. objKeys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }];
  132. } else if (isArray(filter)) {
  133. objKeys = filter;
  134. } else {
  135. var keys = Object.keys(obj);
  136. objKeys = sort ? keys.sort(sort) : keys;
  137. }
  138. var encodedPrefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix);
  139. var adjustedPrefix = commaRoundTrip && isArray(obj) && obj.length === 1 ? encodedPrefix + '[]' : encodedPrefix;
  140. if (allowEmptyArrays && isArray(obj) && obj.length === 0) {
  141. return adjustedPrefix + '[]';
  142. }
  143. for (var j = 0; j < objKeys.length; ++j) {
  144. var key = objKeys[j];
  145. var value = typeof key === 'object' && key && typeof key.value !== 'undefined'
  146. ? key.value
  147. : obj[key];
  148. if (skipNulls && value === null) {
  149. continue;
  150. }
  151. var encodedKey = allowDots && encodeDotInKeys ? String(key).replace(/\./g, '%2E') : String(key);
  152. var keyPrefix = isArray(obj)
  153. ? typeof generateArrayPrefix === 'function' ? generateArrayPrefix(adjustedPrefix, encodedKey) : adjustedPrefix
  154. : adjustedPrefix + (allowDots ? '.' + encodedKey : '[' + encodedKey + ']');
  155. sideChannel.set(object, step);
  156. var valueSideChannel = getSideChannel();
  157. valueSideChannel.set(sentinel, sideChannel);
  158. pushToArray(values, stringify(
  159. value,
  160. keyPrefix,
  161. generateArrayPrefix,
  162. commaRoundTrip,
  163. allowEmptyArrays,
  164. strictNullHandling,
  165. skipNulls,
  166. encodeDotInKeys,
  167. generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder,
  168. filter,
  169. sort,
  170. allowDots,
  171. serializeDate,
  172. format,
  173. formatter,
  174. encodeValuesOnly,
  175. charset,
  176. valueSideChannel
  177. ));
  178. }
  179. return values;
  180. };
  181. var normalizeStringifyOptions = function normalizeStringifyOptions(opts) {
  182. if (!opts) {
  183. return defaults;
  184. }
  185. if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') {
  186. throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided');
  187. }
  188. if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') {
  189. throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided');
  190. }
  191. if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') {
  192. throw new TypeError('Encoder has to be a function.');
  193. }
  194. var charset = opts.charset || defaults.charset;
  195. if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
  196. throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
  197. }
  198. var format = formats['default'];
  199. if (typeof opts.format !== 'undefined') {
  200. if (!has.call(formats.formatters, opts.format)) {
  201. throw new TypeError('Unknown format option provided.');
  202. }
  203. format = opts.format;
  204. }
  205. var formatter = formats.formatters[format];
  206. var filter = defaults.filter;
  207. if (typeof opts.filter === 'function' || isArray(opts.filter)) {
  208. filter = opts.filter;
  209. }
  210. var arrayFormat;
  211. if (opts.arrayFormat in arrayPrefixGenerators) {
  212. arrayFormat = opts.arrayFormat;
  213. } else if ('indices' in opts) {
  214. arrayFormat = opts.indices ? 'indices' : 'repeat';
  215. } else {
  216. arrayFormat = defaults.arrayFormat;
  217. }
  218. if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') {
  219. throw new TypeError('`commaRoundTrip` must be a boolean, or absent');
  220. }
  221. var allowDots = typeof opts.allowDots === 'undefined' ? opts.encodeDotInKeys === true ? true : defaults.allowDots : !!opts.allowDots;
  222. return {
  223. addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix,
  224. allowDots: allowDots,
  225. allowEmptyArrays: typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays,
  226. arrayFormat: arrayFormat,
  227. charset: charset,
  228. charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
  229. commaRoundTrip: !!opts.commaRoundTrip,
  230. delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter,
  231. encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode,
  232. encodeDotInKeys: typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys,
  233. encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder,
  234. encodeValuesOnly: typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly,
  235. filter: filter,
  236. format: format,
  237. formatter: formatter,
  238. serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate,
  239. skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls,
  240. sort: typeof opts.sort === 'function' ? opts.sort : null,
  241. strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling
  242. };
  243. };
  244. module.exports = function (object, opts) {
  245. var obj = object;
  246. var options = normalizeStringifyOptions(opts);
  247. var objKeys;
  248. var filter;
  249. if (typeof options.filter === 'function') {
  250. filter = options.filter;
  251. obj = filter('', obj);
  252. } else if (isArray(options.filter)) {
  253. filter = options.filter;
  254. objKeys = filter;
  255. }
  256. var keys = [];
  257. if (typeof obj !== 'object' || obj === null) {
  258. return '';
  259. }
  260. var generateArrayPrefix = arrayPrefixGenerators[options.arrayFormat];
  261. var commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip;
  262. if (!objKeys) {
  263. objKeys = Object.keys(obj);
  264. }
  265. if (options.sort) {
  266. objKeys.sort(options.sort);
  267. }
  268. var sideChannel = getSideChannel();
  269. for (var i = 0; i < objKeys.length; ++i) {
  270. var key = objKeys[i];
  271. var value = obj[key];
  272. if (options.skipNulls && value === null) {
  273. continue;
  274. }
  275. pushToArray(keys, stringify(
  276. value,
  277. key,
  278. generateArrayPrefix,
  279. commaRoundTrip,
  280. options.allowEmptyArrays,
  281. options.strictNullHandling,
  282. options.skipNulls,
  283. options.encodeDotInKeys,
  284. options.encode ? options.encoder : null,
  285. options.filter,
  286. options.sort,
  287. options.allowDots,
  288. options.serializeDate,
  289. options.format,
  290. options.formatter,
  291. options.encodeValuesOnly,
  292. options.charset,
  293. sideChannel
  294. ));
  295. }
  296. var joined = keys.join(options.delimiter);
  297. var prefix = options.addQueryPrefix === true ? '?' : '';
  298. if (options.charsetSentinel) {
  299. if (options.charset === 'iso-8859-1') {
  300. // encodeURIComponent('&#10003;'), the "numeric entity" representation of a checkmark
  301. prefix += 'utf8=%26%2310003%3B&';
  302. } else {
  303. // encodeURIComponent('✓')
  304. prefix += 'utf8=%E2%9C%93&';
  305. }
  306. }
  307. return joined.length > 0 ? prefix + joined : '';
  308. };