const uriregexp = new RegExp('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?'); export interface URLObject { protocol: string auth: string host: string pathname: string search: string hash: string } export function normalize(path: string) { var segs = path.split("/"); var result = []; var offset = 0; segs.forEach(function (seg, index) { if (seg == "..") { if (result.length > offset) { result.pop(); } else { result.push(".."); } } else if (seg == ".") { return } else if (seg) { result.push(seg) } else if (index < segs.length - 1 && index != 0) { return } else { result.push(""); } }) return result.join("/"); } export function parse(url: string) { var match = url.match(uriregexp) || {}; var authority = match[4]; var authReg = /((([\w]+)(:\w+)?)?@)?([\w.]+)(:(\d+))?/i; if (authority) { var authMatch = authority.match(authReg) || []; } else { var authMatch = [] as any; } return { protocol: match[1] || null , pathname: match[5] || null , path: match[5] && match[6] && match[5] + match[6] || match[5] || match[6] || null , query: match[7] || null , hash: match[8] || null , port: authMatch[7] || null , auth: authMatch[2] || null , hostname: authMatch[5] || null , host: (authMatch[5] || "") + (authMatch[6] || "") || null , search: match[6] || null , href: url } } export function format(urlObject: URLObject) { var result = ""; if (urlObject.protocol) result += urlObject.protocol + "//"; if (urlObject.auth) result += urlObject.auth + "@"; if (urlObject.host) result += urlObject.host; if (urlObject.pathname) { result += urlObject.pathname } else { //nodejs will add a / if no pathname present result += "/" } if (urlObject.search) result += urlObject.search; if (urlObject.hash) result += urlObject.hash; return result; } export function resolve(base: string, target: string) { if (target == "") return base; var targetObject = parse(target); if (targetObject.protocol) return target; var baseObject = parse(base); // in RFC resolve with #hash will result in #hash // in nodejs it will change the base hash if (target[0] == "#") { baseObject.hash = targetObject.hash; return format(baseObject) } targetObject.protocol = baseObject.protocol; if (!targetObject.auth) { targetObject.auth = baseObject.auth || ""; } if (!targetObject.host) { targetObject.host = baseObject.host if (targetObject.pathname) { var targetSegments = targetObject.pathname.split("/"); if (targetObject.pathname[0] == "/") { // so only depends on targetObject baseObject.pathname = ""; } var baseSegments = baseObject.pathname.split("/"); baseSegments.pop(); baseSegments = baseSegments.filter(function (seg) { return seg != "." }); targetSegments.forEach(function (tseg, index) { if (tseg == "..") { // resolve xxx://root/ with ../../../file result in xxx://root/file // not xxx://root/../../../file // but when no scheme/host available it behaves as RFC specs // I will honour nodejs behavior. if (targetSegments.host) { if (baseSegments.length > 0 && baseSegments[baseSegments.length - 1] != "..") { baseSegments.pop(); } else { baseSegments.push(".."); } } else { if (baseSegments.length > 0) { baseSegments.pop(); } } } else if (tseg != ".") { baseSegments.push(tseg); } else if (tseg == "." && index == targetSegments.length - 1) { baseSegments.push("") } }) var _baseSegments = [] var offset = 0; var result = baseSegments.join("/"); // here also nodejs will prepend a / if it's empty // if ((baseObject.pathname[0] == "/" || targetObject.pathname[0]) && result[0] != "/") { result = "/" + result; } var last = targetSegments[targetSegments.length - 1]; if (last == ".." && result[result.length - 1] != "/") { result += "/"; } result = normalize(result); targetObject.pathname = result; } if (!targetObject.pathname) { targetObject.pathname = baseObject.pathname; } } return format(targetObject); }