Browse Source

Rewrote QueryString.parse to make it smaller and more effective.

Also added ability to parse foo.bar=4 equal to foo[bar]=4
Added tests for this as well
v0.7.4-release
Dmitry Baranovskiy 15 years ago
committed by Ryan Dahl
parent
commit
f8ca6b383c
  1. 166
      lib/querystring.js
  2. 9
      test/simple/test-querystring.js

166
lib/querystring.js

@ -1,7 +1,7 @@
// Query String Utilities
var QueryString = exports;
var urlDecode = process.binding('http_parser').urlDecode;
var urlDecode = process.binding("http_parser").urlDecode;
// a safe fast alternative to decodeURIComponent
QueryString.unescape = urlDecode;
@ -26,24 +26,24 @@ var stack = [];
* @static
*/
QueryString.stringify = QueryString.encode = function (obj, sep, eq, munge, name) {
munge = typeof(munge) == "undefined" || munge;
munge = typeof munge == "undefined" || munge;
sep = sep || "&";
eq = eq || "=";
if (obj == null || typeof(obj) === 'function') {
return name ? QueryString.escape(name) + eq : '';
if (obj == null || typeof obj == "function") {
return name ? QueryString.escape(name) + eq : "";
}
if (isBool(obj)) obj = +obj;
if (isBool(obj)) {
obj = +obj;
}
if (isNumber(obj) || isString(obj)) {
return QueryString.escape(name) + eq + QueryString.escape(obj);
}
if (isA(obj, [])) {
var s = [];
name = name+(munge ? '[]' : '');
for (var i = 0, l = obj.length; i < l; i ++) {
s.push( QueryString.stringify(obj[i], sep, eq, munge, name) );
}
return s.join(sep);
name = name + (munge ? "[]" : "");
return obj.map(function (item) {
return QueryString.stringify(item, sep, eq, munge, name);
}).join(sep);
}
// now we know it's an object.
@ -54,107 +54,75 @@ QueryString.stringify = QueryString.encode = function (obj, sep, eq, munge, name
stack.push(obj);
var s = [];
var begin = name ? name + '[' : '';
var end = name ? ']' : '';
var keys = Object.keys(obj);
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
var n = begin + key + end;
s.push(QueryString.stringify(obj[key], sep, eq, munge, n));
}
var begin = name ? name + "[" : "",
end = name ? "]" : "",
keys = Object.keys(obj),
n,
s = Object.keys(obj).map(function (key) {
n = begin + key + end;
return QueryString.stringify(obj[key], sep, eq, munge, n);
}).join(sep);
stack.pop();
s = s.join(sep);
if (!s && name) return name + "=";
if (!s && name) {
return name + "=";
}
return s;
};
QueryString.parse = QueryString.decode = function (qs, sep, eq) {
return (qs || '')
.split(sep||"&")
.map(pieceParser(eq||"="))
.reduce(mergeParams);
};
// matches .xxxxx or [xxxxx] or ['xxxxx'] or ["xxxxx"] with optional [] at the end
var chunks = /(?:(?:^|\.)([^\[\(\.]+)(?=\[|\.|$|\()|\[([^"'][^\]]*?)\]|\["([^\]"]*?)"\]|\['([^\]']*?)'\])(\[\])?/g;
// Parse a key=val string.
// These can get pretty hairy
// example flow:
// parse(foo[bar][][bla]=baz)
// return parse(foo[bar][][bla],"baz")
// return parse(foo[bar][], {bla : "baz"})
// return parse(foo[bar], [{bla:"baz"}])
// return parse(foo, {bar:[{bla:"baz"}]})
// return {foo:{bar:[{bla:"baz"}]}}
var slicerPattern = /(.*)\[([^\]]*)\]$/;
var pieceParser = function (eq) {
return function parsePiece (key, val) {
if (arguments.length !== 2) {
// key=val, called from the map/reduce
key = key.split(eq);
return parsePiece(QueryString.unescape(key.shift(), true),
QueryString.unescape(key.join(eq), true));
}
var sliced = slicerPattern.exec(key);
if (!sliced) {
var ret = {};
if (key) ret[key] = val;
return ret;
}
// ["foo[][bar][][baz]", "foo[][bar][]", "baz"]
var tail = sliced[2], head = sliced[1];
// array: key[]=val
if (!tail) return parsePiece(head, [val]);
// obj: key[subkey]=val
var ret = {};
ret[tail] = val;
return parsePiece(head, ret);
};
QueryString.parse = QueryString.decode = function (qs, sep, eq) {
var obj = {};
String(qs).split(sep || "&").map(function (keyValue) {
var res = obj,
next,
kv = keyValue.split(eq || "="),
key = QueryString.unescape(kv.shift(), true),
value = QueryString.unescape(kv.join(eq || "="), true);
key.replace(chunks, function (all, name, nameInBrackets, nameIn2Quotes, nameIn1Quotes, isArray, offset) {
var end = offset + all.length == key.length;
name = name || nameInBrackets || nameIn2Quotes || nameIn1Quotes;
next = end ? value : {};
next = next && (+next == next ? +next : next);
if (Array.isArray(res[name])) {
res[name].push(next);
res = next;
} else {
if (name in res) {
if (isArray || end) {
res = (res[name] = [res[name], next])[1];
} else {
res = res[name];
}
} else {
if (isArray) {
res = (res[name] = [next])[0];
} else {
res = res[name] = next;
}
}
}
});
});
return obj;
};
// the reducer function that merges each query piece together into one set of params
function mergeParams (params, addition) {
return (
// if it's uncontested, then just return the addition.
(!params) ? addition
// if the existing value is an array, then concat it.
: (isA(params, [])) ? params.concat(addition)
// if the existing value is not an array, and either are not objects, arrayify it.
: (!isA(params, {}) || !isA(addition, {})) ? [params].concat(addition)
// else merge them as objects, which is a little more complex
: mergeObjects(params, addition)
);
}
// Merge two *objects* together. If this is called, we've already ruled
// out the simple cases, and need to do a loop.
function mergeObjects (params, addition) {
var keys = Object.keys(addition);
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
if (key) {
params[key] = mergeParams(params[key], addition[key]);
}
}
return params;
}
function isA (thing, canon) {
// special case for null and undefined
if (thing == null || canon == null) {
return thing === canon;
}
return Object.getPrototypeOf(Object(thing)) == Object.getPrototypeOf(Object(canon));
// special case for null and undefined
if (thing == null || canon == null) {
return thing === canon;
}
return Object.getPrototypeOf(Object(thing)) == Object.getPrototypeOf(Object(canon));
}
function isBool (thing) {
return isA(thing, true);
return isA(thing, true);
}
function isNumber (thing) {
return isA(thing, 0) && isFinite(thing);
return isA(thing, 0) && isFinite(thing);
}
function isString (thing) {
return isA(thing, "");
}
return isA(thing, "");
}

9
test/simple/test-querystring.js

@ -29,6 +29,15 @@ var qsTestCases = [
["foo[bar][bla]=baz&foo[bar][bla]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
["foo[bar][][bla]=baz&foo[bar][][bla]=blo", "foo%5Bbar%5D%5B%5D%5Bbla%5D=baz&foo%5Bbar%5D%5B%5D%5Bbla%5D=blo", {"foo":{"bar":[{"bla":"baz"},{"bla":"blo"}]}}],
["foo[bar][bla][]=baz&foo[bar][bla][]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
["foo.bar.bla=baz&foo.bar.bla=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
["foo.bar[].bla=baz&foo[bar][][bla]=blo", "foo%5Bbar%5D%5B%5D%5Bbla%5D=baz&foo%5Bbar%5D%5B%5D%5Bbla%5D=blo", {"foo":{"bar":[{"bla":"baz"},{"bla":"blo"}]}}],
["foo[bar].bla[]=baz&foo.bar[bla][]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
["foo['bar']['bla']=baz&foo[\"bar\"][\"bla\"]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
["foo['bar'][]['bla']=baz&foo['bar'][][\"bla\"]=blo", "foo%5Bbar%5D%5B%5D%5Bbla%5D=baz&foo%5Bbar%5D%5B%5D%5Bbla%5D=blo", {"foo":{"bar":[{"bla":"baz"},{"bla":"blo"}]}}],
["foo[bar][\"bla\"][]=baz&foo[\"bar\"][bla][]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
[" foo = bar ", "%20foo%20=%20bar%20", {" foo ":" bar "}],
["foo=%zx", "foo=%25zx", {"foo":"%zx"}],
["foo=%EF%BF%BD", "foo=%EF%BF%BD", {"foo" : "\ufffd" }]

Loading…
Cancel
Save