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