summaryrefslogtreecommitdiff
path: root/lib/querystring.js
diff options
context:
space:
mode:
authorisaacs <i@foohack.com>2010-01-03 23:14:12 -0800
committerRyan Dahl <ry@tinyclouds.org>2010-01-04 21:03:54 -0800
commit7ff04c1f86d85876225fe7c3059efaedca8ed14a (patch)
treeb82845df2d8f676c5b712be53807e3729823c0ff /lib/querystring.js
parentd6fe7fb4c3f3ba0cb0da3f16255e0b5d9fbec8da (diff)
downloadnode-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.js177
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))
+ );
+};