Pull in the uri.js from Narwhal and create tests, stripping out the cruft from a...
authorisaacs <i@foohack.com>
Fri, 11 Dec 2009 23:25:35 +0000 (15:25 -0800)
committerRyan Dahl <ry@tinyclouds.org>
Fri, 18 Dec 2009 17:56:59 +0000 (18:56 +0100)
lib/uri.js [new file with mode: 0644]
test/mjsunit/test-uri.js [new file with mode: 0644]

diff --git a/lib/uri.js b/lib/uri.js
new file mode 100644 (file)
index 0000000..0a530cb
--- /dev/null
@@ -0,0 +1,236 @@
+/**
+ * uri.js
+ * A URI parser, compliant with assorted RFCs, providing parsing and resolution utilities.
+ **/
+
+exports.parse = uri_parse;
+exports.format = uri_format;
+exports.resolve = uri_resolve;
+exports.resolveObject = uri_resolveObject;
+
+
+/**** expressionKeys
+  members of a parsed URI object that you get
+  from evaluting the strict regular expression.
+*/
+var expressionKeys = [
+    "url",
+    "protocol",
+    "authorityRoot",
+    "authority",
+      "userInfo",
+        "user",
+        "password",
+      "domain",
+      "port",
+    "path",
+      "root",
+      "directory",
+      "file",
+    "query",
+    "anchor"
+  ],
+  strictExpression = new RegExp( /* url */
+    "^" +
+    "(?:" +
+      "([^:/?#]+):" + /* protocol */
+    ")?" +
+    "(?:" +
+      "(//)" + /* authorityRoot */
+      "(" + /* authority */
+        "(?:" +
+          "(" + /* userInfo */
+            "([^:@/]*)" + /* user */
+            ":?" +
+            "([^@/]*)" + /* password */
+          ")?" +
+          "@" +
+        ")?" +
+        "([^:/?#]*)" + /* domain */
+        "(?::(\\d*))?" + /* port */
+      ")" +
+    ")?" +
+    "(" + /* path */
+      "(/?)" + /* root */
+      "((?:[^?#/]*/)*)" +
+      "([^?#]*)" + /* file */
+    ")" +
+    "(?:\\?([^#]*))?" + /* query */
+    "(?:#(.*))?" /*anchor */
+  );
+
+/**** parse
+  a URI parser function that uses the `strictExpression`
+  and `expressionKeys` and returns an `Object`
+  mapping all `keys` to values.
+*/
+function uri_parse (url) {
+  var items = {},
+    parts = strictExpression.exec(url);
+  
+  for (var i = 0; i < parts.length; i++) {
+    items[expressionKeys[i]] = parts[i] ? parts[i] : "";
+  }
+
+  items.root = (items.root || items.authorityRoot) ? '/' : '';
+
+  items.directories = items.directory.split("/");
+  if (items.directories[items.directories.length - 1] == "") {
+    items.directories.pop();
+  }
+
+  /* normalize */
+  var directories = [];
+  for (var i = 0; i < items.directories.length; i++) {
+    var directory = items.directories[i];
+    if (directory == '.') {
+    } else if (directory == '..') {
+      if (directories.length && directories[directories.length - 1] != '..')
+        directories.pop();
+      else
+        directories.push('..');
+    } else {
+      directories.push(directory);
+    }
+  }
+  items.directories = directories;
+
+  items.domains = items.domain.split(".");
+
+  return items;
+};
+
+
+/**** format
+  accepts a parsed URI object and returns
+  the corresponding string.
+*/
+function uri_format (object) {
+  if (typeof(object) == 'undefined')
+    throw new Error("UrlError: URL undefined for urls#format");
+  if (object instanceof String || typeof(object) === 'string')
+    return object;
+  var domain =
+    object.domains ?
+    object.domains.join(".") :
+    object.domain;
+  var userInfo = (
+      object.user ||
+      object.password 
+    ) ? (
+      (object.user || "") + 
+      (object.password ? ":" + object.password : "") 
+    ) :
+    object.userInfo;
+  var authority = (
+      userInfo ||
+      domain ||
+      object.port
+    ) ? (
+      (userInfo ? userInfo + "@" : "") +
+      (domain || "") + 
+      (object.port ? ":" + object.port : "")
+    ) :
+    object.authority || "";
+    
+  var directory =
+    object.directories ?
+    object.directories.join("/") :
+    object.directory;
+  var path =
+    directory || object.file ?
+    (
+      (directory ? directory + "/" : "") +
+      (object.file || "")
+    ) :
+    object.path;
+  var authorityRoot = 
+    object.authorityRoot
+    || authority ? "//" : "";
+    
+  return object.url = ((
+    (object.protocol ? object.protocol + ":" : "") +
+    (authorityRoot) +
+    (authority) +
+    (object.root || (authority && path) ? "/" : "") +
+    (path ? path : "") +
+    (object.query ? "?" + object.query : "") +
+    (object.anchor ? "#" + object.anchor : "")
+  ) || object.url || "");
+};
+
+/**** resolveObject
+  returns an object representing a URL resolved from
+  a relative location and a source location.
+*/
+function uri_resolveObject (source, relative) {
+  if (!source) 
+    return relative;
+
+  source = uri_parse(source);
+  relative = uri_parse(relative);
+
+  if (relative.url == "")
+    return source;
+
+  delete source.url;
+  delete source.authority;
+  delete source.domain;
+  delete source.userInfo;
+  delete source.path;
+  delete source.directory;
+
+  if (
+    relative.protocol && relative.protocol != source.protocol ||
+    relative.authority && relative.authority != source.authority
+  ) {
+    source = relative;
+  } else {
+    if (relative.root) {
+      source.directories = relative.directories;
+    } else {
+
+      var directories = relative.directories;
+      for (var i = 0; i < directories.length; i++) {
+        var directory = directories[i];
+        if (directory == ".") {
+        } else if (directory == "..") {
+          if (source.directories.length) {
+            source.directories.pop();
+          } else {
+            source.directories.push('..');
+          }
+        } else {
+          source.directories.push(directory);
+        }
+      }
+
+      if (relative.file == ".") {
+        relative.file = "";
+      } else if (relative.file == "..") {
+        source.directories.pop();
+        relative.file = "";
+      }
+    }
+  }
+
+  if (relative.root)
+    source.root = relative.root;
+  if (relative.protcol)
+    source.protocol = relative.protocol;
+  if (!(!relative.path && relative.anchor))
+    source.file = relative.file;
+  source.query = relative.query;
+  source.anchor = relative.anchor;
+
+  return source;
+};
+
+
+/**** resolve
+  returns a URL resovled to a relative URL from a source URL.
+*/
+function uri_resolve (source, relative) {
+  return uri_format(uri_resolveObject(source, relative));
+};
+
diff --git a/test/mjsunit/test-uri.js b/test/mjsunit/test-uri.js
new file mode 100644 (file)
index 0000000..13ac656
--- /dev/null
@@ -0,0 +1,191 @@
+process.mixin(require("./common"));
+
+var uri = require("uri"),
+  sys = require("sys");
+
+// URIs to parse, and expected data
+// { url : parsed }
+var parseTests = {
+  "http://www.narwhaljs.org/blog/categories?id=news" : {
+    "url": "http://www.narwhaljs.org/blog/categories?id=news",
+    "protocol": "http",
+    "authorityRoot": "//",
+    "authority": "www.narwhaljs.org",
+    "userInfo": "",
+    "user": "",
+    "password": "",
+    "domain": "www.narwhaljs.org",
+    "port": "",
+    "path": "/blog/categories",
+    "root": "/",
+    "directory": "blog/",
+    "file": "categories",
+    "query": "id=news",
+    "anchor": "",
+    "directories": [
+      "blog"
+    ],
+    "domains": [
+      "www",
+      "narwhaljs",
+      "org"
+    ]
+  },
+  "http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=" : {
+    "url": "http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=",
+    "protocol": "http",
+    "authorityRoot": "//",
+    "authority": "mt0.google.com",
+    "userInfo": "",
+    "user": "",
+    "password": "",
+    "domain": "mt0.google.com",
+    "port": "",
+    "path": "/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=",
+    "root": "/",
+    "directory": "vt/",
+    "file": "lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=",
+    "query": "",
+    "anchor": "",
+    "directories": [
+      "vt"
+    ],
+    "domains": [
+      "mt0",
+      "google",
+      "com"
+    ]
+  },
+  "http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=" : {
+    "url": "http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=",
+    "protocol": "http",
+    "authorityRoot": "//",
+    "authority": "mt0.google.com",
+    "userInfo": "",
+    "user": "",
+    "password": "",
+    "domain": "mt0.google.com",
+    "port": "",
+    "path": "/vt/lyrs=m@114",
+    "root": "/",
+    "directory": "vt/",
+    "file": "lyrs=m@114",
+    "query": "??&hl=en&src=api&x=2&y=2&z=3&s=",
+    "anchor": "",
+    "directories": [
+      "vt"
+    ],
+    "domains": [
+      "mt0",
+      "google",
+      "com"
+    ]
+  },
+  "http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=" : {
+   "url": "http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=",
+   "protocol": "http",
+   "authorityRoot": "//",
+   "authority": "user:pass@mt0.google.com",
+   "userInfo": "user:pass",
+   "user": "user",
+   "password": "pass",
+   "domain": "mt0.google.com",
+   "port": "",
+   "path": "/vt/lyrs=m@114",
+   "root": "/",
+   "directory": "vt/",
+   "file": "lyrs=m@114",
+   "query": "??&hl=en&src=api&x=2&y=2&z=3&s=",
+   "anchor": "",
+   "directories": [
+    "vt"
+   ],
+   "domains": [
+    "mt0",
+    "google",
+    "com"
+   ]
+  },
+  "file:///etc/passwd" : {
+   "url": "file:///etc/passwd",
+   "protocol": "file",
+   "authorityRoot": "//",
+   "authority": "",
+   "userInfo": "",
+   "user": "",
+   "password": "",
+   "domain": "",
+   "port": "",
+   "path": "/etc/passwd",
+   "root": "/",
+   "directory": "etc/",
+   "file": "passwd",
+   "query": "",
+   "anchor": "",
+   "directories": [
+    "etc"
+   ],
+   "domains": [
+    ""
+   ]
+  },
+  "file:///etc/node/" : {
+   "url": "file:///etc/node/",
+   "protocol": "file",
+   "authorityRoot": "//",
+   "authority": "",
+   "userInfo": "",
+   "user": "",
+   "password": "",
+   "domain": "",
+   "port": "",
+   "path": "/etc/node/",
+   "root": "/",
+   "directory": "etc/node/",
+   "file": "",
+   "query": "",
+   "anchor": "",
+   "directories": [
+    "etc",
+    "node"
+   ],
+   "domains": [
+    ""
+   ]
+  }
+};
+for (var url in parseTests) {
+  var actual = uri.parse(url),
+    expected = parseTests[url];
+  for (var i in expected) {
+    // sys.debug(i);
+    // sys.debug("expect: " + JSON.stringify(expected[i]));
+    // sys.debug("actual: " + JSON.stringify(actual[i]));
+    var e = JSON.stringify(expected[i]),
+      a = JSON.stringify(actual[i]);
+    assert.equal(e, a, "parse(" + url + ")."+i+" == "+e+"\nactual: "+a);
+  }
+  
+  var expected = url,
+    actual = uri.format(parseTests[url]);
+  assert.equal(expected, actual, "format("+url+") == "+url+"\nactual:"+actual);
+}
+
+[
+  // [from, path, expected]
+  ["/foo/bar/baz", "quux", "/foo/bar/quux"],
+  ["/foo/bar/baz", "quux/asdf", "/foo/bar/quux/asdf"],
+  ["/foo/bar/baz", "quux/baz", "/foo/bar/quux/baz"], //TODO: failing test
+  ["/foo/bar/baz", "../quux/baz", "/foo/quux/baz"], //TODO: failing test
+  ["/foo/bar/baz", "/bar", "/bar"],
+  ["/foo/bar/baz/", "quux", "/foo/bar/baz/quux"],
+  ["/foo/bar/baz/", "quux/baz", "/foo/bar/baz/quux/baz"],
+  ["/foo/bar/baz", "../../../../../../../../quux/baz", "/quux/baz"]
+].forEach(function (relativeTest) {
+  var a = uri.resolve(relativeTest[0], relativeTest[1]),
+    e = relativeTest[2];
+  assert.equal(e, a,
+    "resolve("+[relativeTest[0], relativeTest[1]]+") == "+e+
+    "\n  actual="+a);
+});
+