diff options
author | isaacs <i@foohack.com> | 2010-01-03 23:14:12 -0800 |
---|---|---|
committer | Ryan Dahl <ry@tinyclouds.org> | 2010-01-04 21:03:54 -0800 |
commit | 7ff04c1f86d85876225fe7c3059efaedca8ed14a (patch) | |
tree | b82845df2d8f676c5b712be53807e3729823c0ff /lib/querystring.js | |
parent | d6fe7fb4c3f3ba0cb0da3f16255e0b5d9fbec8da (diff) | |
download | node-7ff04c1f86d85876225fe7c3059efaedca8ed14a.tar.gz |
Add URL and QueryString modules, and tests for each.
Also, make a slight change from original on url-module to put the
spacePattern into the function. On closer inspection, it turns out that the
nonlocal-var cost is higher than the compiling-a-regexp cost.
Also, documentation.
Diffstat (limited to 'lib/querystring.js')
-rw-r--r-- | lib/querystring.js | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/lib/querystring.js b/lib/querystring.js new file mode 100644 index 000000000..d258fc297 --- /dev/null +++ b/lib/querystring.js @@ -0,0 +1,177 @@ +// Query String Utilities + +var QueryString = exports; + +QueryString.unescape = function (str, decodeSpaces) { + return decodeURIComponent(decodeSpaces ? str.replace(/\+/g, " ") : str); +}; + +QueryString.escape = function (str) { + return encodeURIComponent(str); +}; + + +var stack = []; +/** + * <p>Converts an arbitrary value to a Query String representation.</p> + * + * <p>Objects with cyclical references will trigger an exception.</p> + * + * @method stringify + * @param obj {Variant} any arbitrary value to convert to query string + * @param sep {String} (optional) Character that should join param k=v pairs together. Default: "&" + * @param eq {String} (optional) Character that should join keys to their values. Default: "=" + * @param name {String} (optional) Name of the current key, for handling children recursively. + * @static + */ +QueryString.stringify = function (obj, sep, eq, name) { + sep = sep || "&"; + eq = eq || "="; + if (isA(obj, null) || isA(obj, undefined) || typeof(obj) === 'function') { + return name ? encodeURIComponent(name) + eq : ''; + } + + if (isBool(obj)) obj = +obj; + if (isNumber(obj) || isString(obj)) { + return encodeURIComponent(name) + eq + encodeURIComponent(obj); + } + if (isA(obj, [])) { + var s = []; + name = name+'[]'; + for (var i = 0, l = obj.length; i < l; i ++) { + s.push( QueryString.stringify(obj[i], sep, eq, name) ); + } + return s.join(sep); + } + // now we know it's an object. + + // Check for cyclical references in nested objects + for (var i = stack.length - 1; i >= 0; --i) if (stack[i] === obj) { + throw new Error("querystring.stringify. Cyclical reference"); + } + + stack.push(obj); + + var s = []; + var begin = name ? name + '[' : ''; + var end = name ? ']' : ''; + for (var i in obj) if (obj.hasOwnProperty(i)) { + var n = begin + i + end; + s.push(QueryString.stringify(obj[i], sep, eq, n)); + } + + stack.pop(); + + s = s.join(sep); + if (!s && name) return name + "="; + return s; +}; + +QueryString.parseQuery = QueryString.parse = function (qs, sep, eq) { + return qs + .split(sep||"&") + .map(pieceParser(eq||"=")) + .reduce(mergeParams); +}; + +// 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 trimmerPattern = /^\s+|\s+$/g, + 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) + ); + } + key = key.replace(trimmerPattern, ''); + if (isString(val)) { + val = val.replace(trimmerPattern, ''); + // convert numerals to numbers + if (!isNaN(val)) { + var numVal = +val; + if (val === numVal.toString(10)) val = numVal; + } + } + 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); + }; +}; + +// 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 the for-in business. +function mergeObjects (params, addition) { + for (var i in addition) if (i && addition.hasOwnProperty(i)) { + params[i] = mergeParams(params[i], addition[i]); + } + return params; +}; + +// duck typing +function isA (thing, canon) { + return ( + // truthiness. you can feel it in your gut. + (!thing === !canon) + // typeof is usually "object" + && typeof(thing) === typeof(canon) + // check the constructor + && Object.prototype.toString.call(thing) === Object.prototype.toString.call(canon) + ); +}; +function isBool (thing) { + return ( + typeof(thing) === "boolean" + || isA(thing, new Boolean(thing)) + ); +}; +function isNumber (thing) { + return ( + typeof(thing) === "number" + || isA(thing, new Number(thing)) + ) && isFinite(thing); +}; +function isString (thing) { + return ( + typeof(thing) === "string" + || isA(thing, new String(thing)) + ); +}; |