/** * Object#toString() ref for stringify(). */ var toString = Object.prototype.toString; /** * Object#hasOwnProperty ref */ var hasOwnProperty = Object.prototype.hasOwnProperty; /** * Array#indexOf shim. */ var indexOf = typeof Array.prototype.indexOf === 'function' ? function(arr, el) { return arr.indexOf(el); } : function(arr, el) { for (var i = 0; i < arr.length; i++) { if (arr[i] === el) return i; } return -1; }; /** * Array.isArray shim. */ var isArray = Array.isArray || function(arr) { return toString.call(arr) == '[object Array]'; }; /** * Object.keys shim. */ var objectKeys = Object.keys || function(obj) { var ret = []; for (var key in obj) ret.push(key); return ret; }; /** * Array#forEach shim. */ var forEach = typeof Array.prototype.forEach === 'function' ? function(arr, fn) { return arr.forEach(fn); } : function(arr, fn) { for (var i = 0; i < arr.length; i++) fn(arr[i]); }; /** * Array#reduce shim. */ var reduce = function(arr, fn, initial) { if (typeof arr.reduce === 'function') return arr.reduce(fn, initial); var res = initial; for (var i = 0; i < arr.length; i++) res = fn(res, arr[i]); return res; }; /** * Create a nullary object if possible */ function createObject() { return Object.create ? Object.create(null) : {}; } /** * Cache non-integer test regexp. */ var isint = /^[0-9]+$/; function promote(parent, key) { if (parent[key].length == 0) return parent[key] = createObject(); var t = createObject(); for (var i in parent[key]) { if (hasOwnProperty.call(parent[key], i)) { t[i] = parent[key][i]; } } parent[key] = t; return t; } function parse(parts, parent, key, val) { var part = parts.shift(); // end if (!part) { if (isArray(parent[key])) { parent[key].push(val); } else if ('object' == typeof parent[key]) { parent[key] = val; } else if ('undefined' == typeof parent[key]) { parent[key] = val; } else { parent[key] = [parent[key], val]; } // array } else { var obj = parent[key] = parent[key] || []; if (']' == part) { if (isArray(obj)) { if ('' != val) obj.push(val); } else if ('object' == typeof obj) { obj[objectKeys(obj).length] = val; } else { obj = parent[key] = [parent[key], val]; } // prop } else if (~indexOf(part, ']')) { part = part.substr(0, part.length - 1); if (!isint.test(part) && isArray(obj)) obj = promote(parent, key); parse(parts, obj, part, val); // key } else { if (!isint.test(part) && isArray(obj)) obj = promote(parent, key); parse(parts, obj, part, val); } } } /** * Merge parent key/val pair. */ function merge(parent, key, val){ if (~indexOf(key, ']')) { var parts = key.split('[') , len = parts.length , last = len - 1; parse(parts, parent, 'base', val); // optimize } else { if (!isint.test(key) && isArray(parent.base)) { var t = createObject(); for (var k in parent.base) t[k] = parent.base[k]; parent.base = t; } set(parent.base, key, val); } return parent; } /** * Compact sparse arrays. */ function compact(obj) { if ('object' != typeof obj) return obj; if (isArray(obj)) { var ret = []; for (var i in obj) { if (hasOwnProperty.call(obj, i)) { ret.push(obj[i]); } } return ret; } for (var key in obj) { obj[key] = compact(obj[key]); } return obj; } /** * Restore Object.prototype. * see pull-request #58 */ function restoreProto(obj) { if (!Object.create) return obj; if (isArray(obj)) return obj; if (obj && 'object' != typeof obj) return obj; for (var key in obj) { if (hasOwnProperty.call(obj, key)) { obj[key] = restoreProto(obj[key]); } } obj.__proto__ = Object.prototype; return obj; } /** * Parse the given obj. */ function parseObject(obj){ var ret = { base: {} }; forEach(objectKeys(obj), function(name){ merge(ret, name, obj[name]); }); return compact(ret.base); } /** * Parse the given str. */ function parseString(str){ var ret = reduce(String(str).split('&'), function(ret, pair){ var eql = indexOf(pair, '=') , brace = lastBraceInKey(pair) , key = pair.substr(0, brace || eql) , val = pair.substr(brace || eql, pair.length) , val = val.substr(indexOf(val, '=') + 1, val.length); // ?foo if ('' == key) key = pair, val = ''; if ('' == key) return ret; return merge(ret, decode(key), decode(val)); }, { base: createObject() }).base; return restoreProto(compact(ret)); } /** * Parse the given query `str` or `obj`, returning an object. * * @param {String} str | {Object} obj * @return {Object} * @api public */ exports.parse = function(str){ if (null == str || '' == str) return {}; return 'object' == typeof str ? parseObject(str) : parseString(str); }; /** * Turn the given `obj` into a query string * * @param {Object} obj * @return {String} * @api public */ var stringify = exports.stringify = function(obj, prefix) { if (isArray(obj)) { return stringifyArray(obj, prefix); } else if ('[object Object]' == toString.call(obj)) { return stringifyObject(obj, prefix); } else if ('string' == typeof obj) { return stringifyString(obj, prefix); } else { return prefix + '=' + encodeURIComponent(String(obj)); } }; /** * Stringify the given `str`. * * @param {String} str * @param {String} prefix * @return {String} * @api private */ function stringifyString(str, prefix) { if (!prefix) throw new TypeError('stringify expects an object'); return prefix + '=' + encodeURIComponent(str); } /** * Stringify the given `arr`. * * @param {Array} arr * @param {String} prefix * @return {String} * @api private */ function stringifyArray(arr, prefix) { var ret = []; if (!prefix) throw new TypeError('stringify expects an object'); for (var i = 0; i < arr.length; i++) { ret.push(stringify(arr[i], prefix + '[' + i + ']')); } return ret.join('&'); } /** * Stringify the given `obj`. * * @param {Object} obj * @param {String} prefix * @return {String} * @api private */ function stringifyObject(obj, prefix) { var ret = [] , keys = objectKeys(obj) , key; for (var i = 0, len = keys.length; i < len; ++i) { key = keys[i]; if ('' == key) continue; if (null == obj[key]) { ret.push(encodeURIComponent(key) + '='); } else { ret.push(stringify(obj[key], prefix ? prefix + '[' + encodeURIComponent(key) + ']' : encodeURIComponent(key))); } } return ret.join('&'); } /** * Set `obj`'s `key` to `val` respecting * the weird and wonderful syntax of a qs, * where "foo=bar&foo=baz" becomes an array. * * @param {Object} obj * @param {String} key * @param {String} val * @api private */ function set(obj, key, val) { var v = obj[key]; if (undefined === v) { obj[key] = val; } else if (isArray(v)) { v.push(val); } else { obj[key] = [v, val]; } } /** * Locate last brace in `str` within the key. * * @param {String} str * @return {Number} * @api private */ function lastBraceInKey(str) { var len = str.length , brace , c; for (var i = 0; i < len; ++i) { c = str[i]; if (']' == c) brace = false; if ('[' == c) brace = true; if ('=' == c && !brace) return i; } } /** * Decode `str`. * * @param {String} str * @return {String} * @api private */ function decode(str) { try { return decodeURIComponent(str.replace(/\+/g, ' ')); } catch (err) { return str; } }