/**
* Object#toString() ref for stringify().
*/
var toString = Object.prototype.toString;
/**
* Cache non-integer test regexp.
*/
var isint = /^[0-9]+$/;
function promote(parent, key) {
if (parent[key].length == 0) return parent[key] = {};
var t = {};
for (var i in parent[key]) t[i] = parent[key][i];
parent[key] = t;
return t;
}
function parse(parts, parent, key, val) {
var part = parts.shift();
// end
if (!part) {
if (Array.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 (Array.isArray(obj)) {
if ('' != val) obj.push(val);
} else if ('object' == typeof obj) {
obj[Object.keys(obj).length] = val;
} else {
obj = parent[key] = [parent[key], val];
}
// prop
} else if (~part.indexOf(']')) {
part = part.substr(0, part.length - 1);
if (!isint.test(part) && Array.isArray(obj)) obj = promote(parent, key);
parse(parts, obj, part, val);
// key
} else {
if (!isint.test(part) && Array.isArray(obj)) obj = promote(parent, key);
parse(parts, obj, part, val);
}
}
}
/**
* Merge parent key/val pair.
*/
function merge(parent, key, val){
if (~key.indexOf(']')) {
var parts = key.split('[')
, len = parts.length
, last = len - 1;
parse(parts, parent, 'base', val);
// optimize
} else {
if (!isint.test(key) && Array.isArray(parent.base)) {
var t = {};
for (var k in parent.base) t[k] = parent.base[k];
parent.base = t;
}
set(parent.base, key, val);
}
return parent;
}
/**
* Parse the given obj.
*/
function parseObject(obj){
var ret = { base: {} };
Object.keys(obj).forEach(function(name){
merge(ret, name, obj[name]);
});
return ret.base;
}
/**
* Parse the given str.
*/
function parseString(str){
return String(str)
.split('&')
.reduce(function(ret, pair){
var eql = pair.indexOf('=')
, brace = lastBraceInKey(pair)
, key = pair.substr(0, brace || eql)
, val = pair.substr(brace || eql, pair.length)
, val = val.substr(val.indexOf('=') + 1, val.length);
// ?foo
if ('' == key) key = pair, val = '';
return merge(ret, decode(key), decode(val));
}, { base: {} }).base;
}
/**
* 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 (Array.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 + '=' + 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 = Object.keys(obj)
, key;
for (var i = 0, len = keys.length; i < len; ++i) {
key = keys[i];
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 (Array.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;
}
}
|