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('✓'), 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 };