Skip to content

Commit 7ff04c1

Browse files
isaacsry
isaacs
authored andcommitted
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.
1 parent d6fe7fb commit 7ff04c1

File tree

5 files changed

+1189
-0
lines changed

5 files changed

+1189
-0
lines changed

doc/api.txt

+93
Original file line numberDiff line numberDiff line change
@@ -1531,6 +1531,99 @@ require("path").exists("/etc/passwd", function (exists) {
15311531
------------------------------------
15321532

15331533

1534+
=== URL Module
1535+
1536+
This module has utilities for URL resolution and parsing.
1537+
1538+
Parsed URL objects have some or all of the following fields, depending on whether or not
1539+
they exist in the URL string. Any parts that are not in the URL string will not be in the
1540+
parsed object. Examples are shown for the URL +"http://user:[email protected]:8080/p/a/t/h?query=string#hash"+
1541+
1542+
+href+::
1543+
The full URL that was originally parsed. Example: +"http://user:[email protected]:8080/p/a/t/h?query=string#hash"+
1544+
1545+
+protocol+::
1546+
The request protocol. Example: +"http:"+
1547+
1548+
+host+::
1549+
The full host portion of the URL, including port and authentication information. Example:
1550+
+"user:[email protected]:8080"+
1551+
1552+
+auth+::
1553+
The authentication information portion of a URL. Example: +"user:pass"+
1554+
1555+
+hostname+::
1556+
Just the hostname portion of the host. Example: +"host.com"+
1557+
1558+
+port+::
1559+
The port number portion of the host. Example: +"8080"+
1560+
1561+
+pathname+::
1562+
The path section of the URL, that comes after the host and before the query, including the
1563+
initial slash if present. Example: +"/p/a/t/h"+
1564+
1565+
+search+::
1566+
The "query string" portion of the URL, including the leading question mark. Example:
1567+
+"?query=string"+
1568+
1569+
+query+::
1570+
Either the "params" portion of the query string, or a querystring-parsed object. Example:
1571+
+"query=string"+ or +{"query":"string"}+
1572+
1573+
+hash+::
1574+
The portion of the URL after the pound-sign. Example: +"#hash"+
1575+
1576+
The following methods are provided by the URL module:
1577+
1578+
+url.parse(urlStr, parseQueryString=false)+::
1579+
Take a URL string, and return an object. Pass +true+ as the second argument to also parse
1580+
the query string using the +querystring+ module.
1581+
1582+
+url.format(urlObj)+::
1583+
Take a parsed URL object, and return a formatted URL string.
1584+
1585+
+url.resolve(from, to)+::
1586+
Take a base URL, and a href URL, and resolve them as a browser would for an anchor tag.
1587+
1588+
1589+
=== Query String Module
1590+
1591+
This module provides utilities for dealing with query strings. It provides the following methods:
1592+
1593+
+querystring.stringify(obj, sep="&", eq="=")+::
1594+
Serialize an object to a query string. Optionally override the default separator and assignment characters.
1595+
Example:
1596+
+
1597+
------------------------------------
1598+
node> require("querystring").stringify({foo:"bar", baz : {quux:"asdf", oof : "rab"}, boo:[1,2,3]})
1599+
"foo=bar&baz%5Bquux%5D=asdf&baz%5Boof%5D=rab&boo%5B%5D=1&boo%5B%5D=2&boo%5B%5D=3"
1600+
------------------------------------
1601+
+
1602+
1603+
+querystring.parse(str, sep="&", eq="=")+::
1604+
Deserialize a query string to an object. Optionally override the default separator and assignment characters.
1605+
+
1606+
------------------------------------
1607+
node> require("querystring").parse("foo=bar&baz%5Bquux%5D=asdf&baz%5Boof%5D=rab&boo%5B%5D=1")
1608+
{
1609+
"foo": "bar",
1610+
"baz": {
1611+
"quux": "asdf",
1612+
"oof": "rab"
1613+
},
1614+
"boo": [
1615+
1
1616+
]
1617+
}
1618+
------------------------------------
1619+
+
1620+
1621+
+querystring.escape+
1622+
The escape function used by +querystring.stringify+, provided so that it could be overridden if necessary.
1623+
1624+
+querystring.unescape+
1625+
The unescape function used by +querystring.parse+, provided so that it could be overridden if necessary.
1626+
15341627
== REPL
15351628

15361629
A Read-Eval-Print-Loop is available both as a standalone program and easily

lib/querystring.js

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Query String Utilities
2+
3+
var QueryString = exports;
4+
5+
QueryString.unescape = function (str, decodeSpaces) {
6+
return decodeURIComponent(decodeSpaces ? str.replace(/\+/g, " ") : str);
7+
};
8+
9+
QueryString.escape = function (str) {
10+
return encodeURIComponent(str);
11+
};
12+
13+
14+
var stack = [];
15+
/**
16+
* <p>Converts an arbitrary value to a Query String representation.</p>
17+
*
18+
* <p>Objects with cyclical references will trigger an exception.</p>
19+
*
20+
* @method stringify
21+
* @param obj {Variant} any arbitrary value to convert to query string
22+
* @param sep {String} (optional) Character that should join param k=v pairs together. Default: "&"
23+
* @param eq {String} (optional) Character that should join keys to their values. Default: "="
24+
* @param name {String} (optional) Name of the current key, for handling children recursively.
25+
* @static
26+
*/
27+
QueryString.stringify = function (obj, sep, eq, name) {
28+
sep = sep || "&";
29+
eq = eq || "=";
30+
if (isA(obj, null) || isA(obj, undefined) || typeof(obj) === 'function') {
31+
return name ? encodeURIComponent(name) + eq : '';
32+
}
33+
34+
if (isBool(obj)) obj = +obj;
35+
if (isNumber(obj) || isString(obj)) {
36+
return encodeURIComponent(name) + eq + encodeURIComponent(obj);
37+
}
38+
if (isA(obj, [])) {
39+
var s = [];
40+
name = name+'[]';
41+
for (var i = 0, l = obj.length; i < l; i ++) {
42+
s.push( QueryString.stringify(obj[i], sep, eq, name) );
43+
}
44+
return s.join(sep);
45+
}
46+
// now we know it's an object.
47+
48+
// Check for cyclical references in nested objects
49+
for (var i = stack.length - 1; i >= 0; --i) if (stack[i] === obj) {
50+
throw new Error("querystring.stringify. Cyclical reference");
51+
}
52+
53+
stack.push(obj);
54+
55+
var s = [];
56+
var begin = name ? name + '[' : '';
57+
var end = name ? ']' : '';
58+
for (var i in obj) if (obj.hasOwnProperty(i)) {
59+
var n = begin + i + end;
60+
s.push(QueryString.stringify(obj[i], sep, eq, n));
61+
}
62+
63+
stack.pop();
64+
65+
s = s.join(sep);
66+
if (!s && name) return name + "=";
67+
return s;
68+
};
69+
70+
QueryString.parseQuery = QueryString.parse = function (qs, sep, eq) {
71+
return qs
72+
.split(sep||"&")
73+
.map(pieceParser(eq||"="))
74+
.reduce(mergeParams);
75+
};
76+
77+
// Parse a key=val string.
78+
// These can get pretty hairy
79+
// example flow:
80+
// parse(foo[bar][][bla]=baz)
81+
// return parse(foo[bar][][bla],"baz")
82+
// return parse(foo[bar][], {bla : "baz"})
83+
// return parse(foo[bar], [{bla:"baz"}])
84+
// return parse(foo, {bar:[{bla:"baz"}]})
85+
// return {foo:{bar:[{bla:"baz"}]}}
86+
var trimmerPattern = /^\s+|\s+$/g,
87+
slicerPattern = /(.*)\[([^\]]*)\]$/;
88+
var pieceParser = function (eq) {
89+
return function parsePiece (key, val) {
90+
if (arguments.length !== 2) {
91+
// key=val, called from the map/reduce
92+
key = key.split(eq);
93+
return parsePiece(
94+
QueryString.unescape(key.shift(), true),
95+
QueryString.unescape(key.join(eq), true)
96+
);
97+
}
98+
key = key.replace(trimmerPattern, '');
99+
if (isString(val)) {
100+
val = val.replace(trimmerPattern, '');
101+
// convert numerals to numbers
102+
if (!isNaN(val)) {
103+
var numVal = +val;
104+
if (val === numVal.toString(10)) val = numVal;
105+
}
106+
}
107+
var sliced = slicerPattern.exec(key);
108+
if (!sliced) {
109+
var ret = {};
110+
if (key) ret[key] = val;
111+
return ret;
112+
}
113+
// ["foo[][bar][][baz]", "foo[][bar][]", "baz"]
114+
var tail = sliced[2], head = sliced[1];
115+
116+
// array: key[]=val
117+
if (!tail) return parsePiece(head, [val]);
118+
119+
// obj: key[subkey]=val
120+
var ret = {};
121+
ret[tail] = val;
122+
return parsePiece(head, ret);
123+
};
124+
};
125+
126+
// the reducer function that merges each query piece together into one set of params
127+
function mergeParams (params, addition) {
128+
return (
129+
// if it's uncontested, then just return the addition.
130+
(!params) ? addition
131+
// if the existing value is an array, then concat it.
132+
: (isA(params, [])) ? params.concat(addition)
133+
// if the existing value is not an array, and either are not objects, arrayify it.
134+
: (!isA(params, {}) || !isA(addition, {})) ? [params].concat(addition)
135+
// else merge them as objects, which is a little more complex
136+
: mergeObjects(params, addition)
137+
);
138+
};
139+
140+
// Merge two *objects* together. If this is called, we've already ruled
141+
// out the simple cases, and need to do the for-in business.
142+
function mergeObjects (params, addition) {
143+
for (var i in addition) if (i && addition.hasOwnProperty(i)) {
144+
params[i] = mergeParams(params[i], addition[i]);
145+
}
146+
return params;
147+
};
148+
149+
// duck typing
150+
function isA (thing, canon) {
151+
return (
152+
// truthiness. you can feel it in your gut.
153+
(!thing === !canon)
154+
// typeof is usually "object"
155+
&& typeof(thing) === typeof(canon)
156+
// check the constructor
157+
&& Object.prototype.toString.call(thing) === Object.prototype.toString.call(canon)
158+
);
159+
};
160+
function isBool (thing) {
161+
return (
162+
typeof(thing) === "boolean"
163+
|| isA(thing, new Boolean(thing))
164+
);
165+
};
166+
function isNumber (thing) {
167+
return (
168+
typeof(thing) === "number"
169+
|| isA(thing, new Number(thing))
170+
) && isFinite(thing);
171+
};
172+
function isString (thing) {
173+
return (
174+
typeof(thing) === "string"
175+
|| isA(thing, new String(thing))
176+
);
177+
};

0 commit comments

Comments
 (0)