diff --git a/Foundation/CPURL.j b/Foundation/CPURL.j new file mode 100644 index 000000000..f11415608 --- /dev/null +++ b/Foundation/CPURL.j @@ -0,0 +1,550 @@ +@import + +@implementation CPURL : CPObject +{ + CPURL _base @accessors(readonly, property=baseURL); + CPString _relative @accessors(readonly, property=relativeString); +} + +- (id)initWithScheme:(CPString)scheme host:(CPString)host path:(CPString)path +{ + var uri = new URI(); + uri.scheme = scheme; + uri.authority = host; + uri.path = path; + [self initWithString:uri.toString()]; +} + +- (id)initWithString:(CPString)URLString +{ + return [self initWithString:URLString relativeToURL:nil]; +} + ++ (id)URLWithString:(CPString)URLString +{ + return [[self alloc] initWithString:URLString]; +} + +- (id)initWithString:(CPString)URLString relativeToURL:(CPURL)baseURL +{ + if (!URI_RE.test(URLString)) + return nil; + + _base = baseURL; + _relative = URLString; + + return self; +} + ++ (id)URLWithString:(CPString)URLString relativeToURL:(CPURL)baseURL +{ + return [[self alloc] initWithString:URLString relativeToURL:baseURL]; +} + +- (CPURL)absoluteURL +{ + var absStr = [self absoluteString]; + + if (absStr !== _relative) + return [[CPURL alloc] initWithString:absStr]; + + return self; +} + +- (CPString)absoluteString +{ + return resolve([_base absoluteString] || "", _relative); +} + +// if absolute, returns same as absoluteString +- (CPString)relativeString +{ + return _relative; +} + +- (CPString)path +{ + var str = [self absoluteString]; + return URI_RE.test(str) ? (parse(str).path || nil) : nil; +} + +// if absolute, returns the same as path +- (CPString)relativePath +{ + return URI_RE.test(_relative) ? (parse(_relative).path || nil) : nil; +} + + +- (CPString)scheme +{ + var str = [self absoluteString]; + return URI_RE.test(str) ? (parse(str).protocol || nil) : nil; +} + +- (CPString)user +{ + var str = [self absoluteString]; + return URI_RE.test(str) ? (parse(str).user || nil) : nil; +} + +- (CPString)password +{ + var str = [self absoluteString]; + return URI_RE.test(str) ? (parse(str).password || nil) : nil; +} + +- (CPString)host +{ + var str = [self absoluteString]; + return URI_RE.test(str) ? (parse(str).domain || nil) : nil; +} + +- (CPString)port +{ + var str = [self absoluteString]; + if (URI_RE.test(str)) { + var port = parse(str).port; + if (port) + return parseInt(port, 10); + } + return nil; +} + +- (CPString)parameterString +{ + var str = [self absoluteString]; + return URI_RE.test(str) ? (parse(str).query || nil) : nil; +} + +- (CPString)fragment +{ + var str = [self absoluteString]; + return URI_RE.test(str) ? (parse(str).anchor || nil) : nil; +} + +- (BOOL)isEqual:(id)anObject +{ + // Is checking if baseURL isEqual correct? Does "identical" mean same object or equivalent values? + return [self relativeString] === [anObject relativeString] && + ([self baseURL] === [anObject baseURL] || [[self baseURL] isEqual:[anObject baseURL]]); +} + +- (CPString)lastPathComponent +{ + var path = [self path]; + return path ? path.split("/").pop() : nil; +} + +- (CPString)pathExtension +{ + var path = [self path], + ext = path.match(/\.(\w+)$/); + return ext ? ext[1] : ""; +} + +- (CPURL)standardizedURL +{ + return [CPURL URLWithString:format(parse(_relative)) relativeToURL:_base]; +} + +- (BOOL)isFileURL +{ + return [self scheme] === "file"; +} + +@end + +@implementation CPURL (CPCoding) + +- (id)initWithCoder:(CPCoder)aCoder +{ + _base = [aCoder decodeObjectForKey:"CPURLBaseKey"]; + _relative = [aCoder decodeObjectForKey:"CPURLRelativeKey"]; + return self; +} + +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [aCoder encodeObject:_base forKey:"CPURLBaseKey"]; + [aCoder encodeObject:_relative forKey:"CPURLRelativeKey"]; +} + +@end + +// original code: http://code.google.com/p/js-uri/ + +// Based on the regex in RFC2396 Appendix B. +var URI_RE = /^(?:([^:\/?\#]+):)?(?:\/\/([^\/?\#]*))?([^?\#]*)(?:\?([^\#]*))?(?:\#(.*))?/; + +/** + * Uniform Resource Identifier (URI) - RFC3986 + */ +var URI = function(str) { + if (!str) str = ""; + var result = str.match(URI_RE); + this.scheme = result[1] || null; + this.authority = result[2] || null; + this.path = result[3] || null; + this.query = result[4] || null; + this.fragment = result[5] || null; +} + +/** + * Convert the URI to a String. + */ +URI.prototype.toString = function () { + var str = ""; + + if (this.scheme) + str += this.scheme + ":"; + + if (this.authority) + str += "//" + this.authority; + + if (this.path) + str += this.path; + + if (this.query) + str += "?" + this.query; + + if (this.fragment) + str += "#" + this.fragment; + + return str; +} + +var parse = function(uri) { + return new URI(uri); +} + +var unescape = function(str, plus) { + return decodeURI(str).replace(/\+/g, " "); +} + +var unescapeComponent = function(str, plus) { + return decodeURIComponent(str).replace(/\+/g, " "); +} + +// from Chiron's HTTP module: + +/**** keys + members of a parsed URI object. +*/ +var keys = [ + "url", + "protocol", + "authorityRoot", + "authority", + "userInfo", + "user", + "password", + "domain", + "domains", + "port", + "path", + "root", + "directory", + "directories", + "file", + "query", + "anchor" +]; + +/**** 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 +*/ +var strictExpression = new RegExp( /* url */ + "^" + + "(?:" + + "([^:/?#]+):" + /* protocol */ + ")?" + + "(?:" + + "(//)" + /* authorityRoot */ + "(" + /* authority */ + "(?:" + + "(" + /* userInfo */ + "([^:@]*)" + /* user */ + ":?" + + "([^:@]*)" + /* password */ + ")?" + + "@" + + ")?" + + "([^:/?#]*)" + /* domain */ + "(?::(\\d*))?" + /* port */ + ")" + + ")?" + + "(" + /* path */ + "(/?)" + /* root */ + "((?:[^?#/]*/)*)" + + "([^?#]*)" + /* file */ + ")" + + "(?:\\?([^#]*))?" + /* query */ + "(?:#(.*))?" /*anchor */ +); + +/**** Parser + returns a URI parser function given + a regular expression that renders + `expressionKeys` and returns an `Object` + mapping all `keys` to values. +*/ +var Parser = function (expression) { + return function (url) { + if (typeof url == "undefined") + throw new Error("HttpError: URL is undefined"); + if (typeof url != "string") return new Object(url); + + var items = {}; + var parts = expression.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; + }; +}; + +/**** parse + a strict URI parser. +*/ +var parse = Parser(strictExpression); + +/**** format + accepts a parsed URI object and returns + the corresponding string. +*/ +var format = function (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; + return ( + (object.protocol ? object.protocol + ":" : "") + + (authority ? "//" + 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. +*/ +var resolveObject = function (source, relative) { + if (!source) + return relative; + + source = parse(source); + relative = 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; +}; + +/**** relativeObject + returns an object representing a relative URL to + a given target URL from a source URL. +*/ +var relativeObject = function (source, target) { + target = parse(target); + source = parse(source); + + delete target.url; + + if ( + target.protocol == source.protocol && + target.authority == source.authority + ) { + delete target.protocol; + delete target.authority; + delete target.userInfo; + delete target.user; + delete target.password; + delete target.domain; + delete target.domains; + delete target.port; + if ( + !!target.root == !!source.root && !( + target.root && + target.directories[0] != source.directories[0] + ) + ) { + delete target.path; + delete target.root; + delete target.directory; + while ( + source.directories.length && + target.directories.length && + target.directories[0] == source.directories[0] + ) { + target.directories.shift(); + source.directories.shift(); + } + while (source.directories.length) { + source.directories.shift(); + target.directories.unshift('..'); + } + + if (!target.root && !target.directories.length && !target.file && source.file) + target.directories.push('.'); + + if (source.file == target.file) + delete target.file; + if (source.query == target.query) + delete target.query; + if (source.anchor == target.anchor) + delete target.anchor; + } + } + + return target; +}; + +/**** resolve + returns a URL resovled to a relative URL from a source URL. +*/ +var resolve = function (source, relative) { + return format(resolveObject(source, relative)); +}; + +/**** relative + returns a relative URL to a target from a source. +*/ +var relative = function (source, target) { + return format(relativeObject(source, target)); +}; diff --git a/Tests/Foundation/CPURLTest.j b/Tests/Foundation/CPURLTest.j new file mode 100644 index 000000000..271772795 --- /dev/null +++ b/Tests/Foundation/CPURLTest.j @@ -0,0 +1,108 @@ +@import "CPURL.j" + +var exampleProtocol = "http", + exampleUser = "foo", + examplePassword = "bar", + exampleHost = "cappuccino.org", + examplePort = 80, + exampleAuthority = exampleUser + ":" + examplePassword + "@" + exampleHost + ":" + examplePort, + examplePathBase = "/contribute/", + examplePathRelative = "coding-style.php", + exampleFullPath = examplePathBase + examplePathRelative, + exampleQuery = "foo=bar", + exampleAnchor = "baz", + exampleURL = exampleProtocol + "://" + exampleAuthority + exampleFullPath + "?" + exampleQuery + "#" + exampleAnchor; + +@implementation CPURLTest : OJTestCase +{ + +} + +- (void)testAbsolute +{ + var pathString = exampleFullPath, + urlString = exampleURL, + url = [CPURL URLWithString:urlString]; + + [self assert:[url baseURL] equals:nil]; + + [self assert:[url relativeString] equals:urlString]; + [self assert:[url absoluteString] equals:urlString]; + + [self assert:[url relativePath] equals:pathString]; + [self assert:[url path] equals:pathString]; + + [self assert:[url absoluteURL] same:url]; + + [self assert:[url scheme] equals:exampleProtocol]; + [self assert:[url user] equals:exampleUser]; + [self assert:[url password] equals:examplePassword]; + [self assert:[url host] equals:exampleHost]; + [self assert:[url port] equals:examplePort]; + [self assert:[url parameterString] equals:exampleQuery]; + [self assert:[url fragment] equals:exampleAnchor]; + + [self assert:[url pathExtension] equals:"php"]; + [self assert:[url lastPathComponent] equals:examplePathRelative]; +} + +- (void)testRelative +{ + var baseString = exampleProtocol + "://" + exampleAuthority + examplePathBase, + pathString = examplePathRelative, + urlString = examplePathRelative + "?" + exampleQuery + "#" + exampleAnchor, + baseURL = [CPURL URLWithString:baseString], + url = [CPURL URLWithString:urlString relativeToURL:baseURL]; + + [self assert:[url baseURL] equals:baseURL]; + + [self assert:[url relativeString] equals:urlString]; + [self assert:[url absoluteString] equals:exampleURL]; + + [self assert:[url relativePath] equals:pathString]; + [self assert:[url path] equals:exampleFullPath]; + + [self assert:[url absoluteURL] notSame:url]; + + [self assert:[url scheme] equals:exampleProtocol]; + [self assert:[url user] equals:exampleUser]; + [self assert:[url password] equals:examplePassword]; + [self assert:[url host] equals:exampleHost]; + [self assert:[url port] equals:examplePort]; + [self assert:[url parameterString] equals:exampleQuery]; + [self assert:[url fragment] equals:exampleAnchor]; + + [self assert:[url pathExtension] equals:"php"]; + [self assert:[url lastPathComponent] equals:examplePathRelative]; +} + +- (void)testRelativeNoBase +{ + var pathString = examplePathRelative, + urlString = examplePathRelative + "?" + exampleQuery + "#" + exampleAnchor, + url = [CPURL URLWithString:urlString]; + + [self assert:[url baseURL] equals:nil]; + + [self assert:[url relativeString] equals:urlString]; + [self assert:[url absoluteString] equals:urlString]; + + [self assert:[url relativePath] equals:pathString]; + [self assert:[url path] equals:pathString]; + + [self assert:[url absoluteURL] same:url]; + + [self assert:[url scheme] equals:nil]; + [self assert:[url user] equals:nil]; + [self assert:[url password] equals:nil]; + [self assert:[url host] equals:nil]; + [self assert:[url port] equals:nil]; + [self assert:[url parameterString] equals:exampleQuery]; + [self assert:[url fragment] equals:exampleAnchor]; + + [self assert:[url pathExtension] equals:"php"]; + [self assert:[url lastPathComponent] equals:examplePathRelative]; + +} + +@end