From f8ca6b383c1cac95ba9a9bcddfe66cc62b09558b Mon Sep 17 00:00:00 2001 From: Dmitry Baranovskiy Date: Tue, 29 Jun 2010 18:24:43 +1000 Subject: [PATCH] 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 --- lib/querystring.js | 166 ++++++++++++++++------------------------ test/simple/test-querystring.js | 9 +++ 2 files changed, 76 insertions(+), 99 deletions(-) diff --git a/lib/querystring.js b/lib/querystring.js index e3b75ba..efd2969 100644 --- a/lib/querystring.js +++ b/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, ""); +} \ No newline at end of file diff --git a/test/simple/test-querystring.js b/test/simple/test-querystring.js index 45d2c35..f3b3301 100644 --- a/test/simple/test-querystring.js +++ b/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" }] -- 2.7.4