Skip to content

Commit

Permalink
Add URL and QueryString modules, and tests for each.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
isaacs authored and ry committed Jan 5, 2010
1 parent d6fe7fb commit 7ff04c1
Show file tree
Hide file tree
Showing 5 changed files with 1,189 additions and 0 deletions.
93 changes: 93 additions & 0 deletions doc/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,99 @@ require("path").exists("/etc/passwd", function (exists) {
------------------------------------


=== URL Module

This module has utilities for URL resolution and parsing.

Parsed URL objects have some or all of the following fields, depending on whether or not
they exist in the URL string. Any parts that are not in the URL string will not be in the
parsed object. Examples are shown for the URL +"http://user:pass@host.com:8080/p/a/t/h?query=string#hash"+

+href+::
The full URL that was originally parsed. Example: +"http://user:pass@host.com:8080/p/a/t/h?query=string#hash"+

+protocol+::
The request protocol. Example: +"http:"+

+host+::
The full host portion of the URL, including port and authentication information. Example:
+"user:pass@host.com:8080"+

+auth+::
The authentication information portion of a URL. Example: +"user:pass"+

+hostname+::
Just the hostname portion of the host. Example: +"host.com"+

+port+::
The port number portion of the host. Example: +"8080"+

+pathname+::
The path section of the URL, that comes after the host and before the query, including the
initial slash if present. Example: +"/p/a/t/h"+

+search+::
The "query string" portion of the URL, including the leading question mark. Example:
+"?query=string"+

+query+::
Either the "params" portion of the query string, or a querystring-parsed object. Example:
+"query=string"+ or +{"query":"string"}+

+hash+::
The portion of the URL after the pound-sign. Example: +"#hash"+

The following methods are provided by the URL module:

+url.parse(urlStr, parseQueryString=false)+::
Take a URL string, and return an object. Pass +true+ as the second argument to also parse
the query string using the +querystring+ module.

+url.format(urlObj)+::
Take a parsed URL object, and return a formatted URL string.

+url.resolve(from, to)+::
Take a base URL, and a href URL, and resolve them as a browser would for an anchor tag.


=== Query String Module

This module provides utilities for dealing with query strings. It provides the following methods:

+querystring.stringify(obj, sep="&", eq="=")+::
Serialize an object to a query string. Optionally override the default separator and assignment characters.
Example:
+
------------------------------------
node> require("querystring").stringify({foo:"bar", baz : {quux:"asdf", oof : "rab"}, boo:[1,2,3]})
"foo=bar&baz%5Bquux%5D=asdf&baz%5Boof%5D=rab&boo%5B%5D=1&boo%5B%5D=2&boo%5B%5D=3"
------------------------------------
+

+querystring.parse(str, sep="&", eq="=")+::
Deserialize a query string to an object. Optionally override the default separator and assignment characters.
+
------------------------------------
node> require("querystring").parse("foo=bar&baz%5Bquux%5D=asdf&baz%5Boof%5D=rab&boo%5B%5D=1")
{
"foo": "bar",
"baz": {
"quux": "asdf",
"oof": "rab"
},
"boo": [
1
]
}
------------------------------------
+

+querystring.escape+
The escape function used by +querystring.stringify+, provided so that it could be overridden if necessary.

+querystring.unescape+
The unescape function used by +querystring.parse+, provided so that it could be overridden if necessary.

== REPL

A Read-Eval-Print-Loop is available both as a standalone program and easily
Expand Down
177 changes: 177 additions & 0 deletions lib/querystring.js
Original file line number Diff line number Diff line change
@@ -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))
);
};
Loading

0 comments on commit 7ff04c1

Please sign in to comment.