diff --git a/Makefile b/Makefile index b92cc31..a51d8f6 100644 --- a/Makefile +++ b/Makefile @@ -5,4 +5,7 @@ test: $(MAKE) lint @NODE_ENV=test ./node_modules/.bin/jest +lib: index.ts + ./node_modules/.bin/tsc + .PHONY: test diff --git a/index.ts b/index.ts index be0b27d..e69fd9d 100644 --- a/index.ts +++ b/index.ts @@ -466,10 +466,12 @@ UrlGrey.prototype.params = function(inUrl){ // TODO relative() // takes an absolutepath and returns a relative one // TODO absolute() // takes a relative path and returns an absolute one. -export default (url: string) => { - return new UrlGrey(url) +export default function urlgrey(url: string) { + return new UrlGrey(url); } +urlgrey.UrlGrey = UrlGrey; + // function addPropertyGetterSetter(propertyName: string, methodName?: string) { // if (!methodName) { // methodName = propertyName; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..0c54f52 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,429 @@ +"use strict"; +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +}; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var _UrlGrey_url; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UrlGrey = void 0; +function lastItem(arr) { + return arr[arr.length - 1]; +} +function urlParse(href) { + if (!href.includes("://")) { + if (href.charAt(0) === "/") { + href = "http://localhost/"; + } + href = "http://" + href; + } + const url = new URL(href); + const colonFreeProtocol = url.protocol.substring(0, url.protocol.length - 1); + const hash = (url.hash && url.hash.charAt(0)) === "#" ? url.hash.substring(1) : url.hash; + const result = { + protocol: colonFreeProtocol, + auth: `${url.username}:${url.password}`, + username: url.username, + password: url.password, + host: url.host, + hostname: url.hostname, + hash, + search: url.search, + pathname: url.pathname, + port: url.port ? parseInt(url.port, 10) : null, + path: (url.pathname || "") + (url.search || ""), + query: url.search ? url.search.slice(1) : url.search, + }; + return result; +} +const getDefaults = function () { + const defaultUrl = "http://localhost:80"; + const defaults = urlParse(defaultUrl); + return defaults; +}; +class UrlGrey { + constructor(url) { + _UrlGrey_url.set(this, void 0); + __classPrivateFieldSet(this, _UrlGrey_url, url, "f"); + this._parsed = null; + } + parsed() { + if (this._parsed) { + return this._parsed; + } + this._parsed = urlParse(__classPrivateFieldGet(this, _UrlGrey_url, "f")); + const defaults = getDefaults(); + this._parsed.protocol = this._parsed.protocol || defaults.protocol; + return this._parsed; + } + getExtendedPath() { + let href = this.getPath(); + href += this.getQueryString() ? "?" + this.getQueryString() : ""; + href += this.getHash() ? "#" + this.getHash() : ""; + return href; + } + extendedPath(url) { + const clone = this.clone(); + const p = urlParse(`http://localhost/${url}`); + clone.parsed().hash = p.hash; + clone.parsed().query = p.query; + return clone.path(p.pathname); + } + port(num) { + if (this.getProtocol() === "file") { + throw new Error("file urls don't have ports"); + } + const clone = this.clone(); + clone.parsed().port = parseInt(`${num}`, 10); + return clone; + } + getPort() { + // getter + const output = this.parsed().port; + if (output) + return output; + switch (this.getProtocol()) { + case "http": + return 80; + case "https": + return 443; + default: + return null; + } + } + getQuery() { + return qsParse(this.parsed().query); + } + clearQuery() { + return this.queryString(""); + } + query(mergeObject) { + // read the object out + const oldQuery = qsParse(this.parsed().query); + for (const [k, v] of Object.entries(mergeObject)) { + if (v === null) { + delete oldQuery[k]; + } + else { + oldQuery[k] = `${v}`; + } + } + const newString = new URLSearchParams(oldQuery).toString(); + return this.queryString(newString); + } + getRawQuery() { + const qObj = {}; + if (this.getQueryString().length === 0) { + return qObj; + } + return this.getQueryString() + .split("&") + .reduce(function (obj, pair) { + const pieces = pair.split("="); + const key = pieces[0]; + const val = pieces[1]; + obj[key] = val; + return obj; + }, qObj); + } + rawQuery(mergeObject) { + if (mergeObject === null || mergeObject === false) { + return this.queryString(""); + } + // read the object out + const oldQuery = qsParse(this.parsed().query); + for (const [k, v] of Object.entries(mergeObject)) { + if (v === null) { + delete oldQuery[k]; + } + else { + oldQuery[k] = `${v}`; + } + } + const pairs = []; + for (const [k, v] of Object.entries(oldQuery)) { + pairs.push(k + "=" + v); + } + const newString = pairs.join("&"); + return this.queryString(newString); + } + getProtocol() { + return this.parsed().protocol; + } + protocol(str) { + const clone = this.clone(); + clone.parsed().protocol = str; + return clone; + } + getUsername() { + return this.parsed().username; + } + username(str) { + const clone = this.clone(); + clone.parsed().username = str; + return clone; + } + getPassword() { + return this.parsed().password; + } + password(str) { + const clone = this.clone(); + clone.parsed().password = str; + return clone; + } + getHostname() { + return this.parsed().hostname; + } + hostname(str) { + const clone = this.clone(); + clone.parsed().hostname = str; + return clone; + } + getHash() { + return this.parsed().hash; + } + hash(str) { + const clone = this.clone(); + clone.parsed().hash = str; + return clone; + } + getPath() { + return this.parsed().pathname; + } + path(input) { + let args = Array.isArray(input) ? input.flat(5) : [input]; + let str = args.join("/"); + str = str.replace(/\/+/g, "/"); // remove double slashes + str = str.replace(/\/$/, ""); // remove all trailing slashes + args = str.split("/"); + for (let i = 0; i < args.length; i++) { + args[i] = this.encode(`${args[i]}`); + } + str = args.join("/"); + if (str[0] !== "/") { + str = "/" + str; + } + const clone = this.clone(); + clone.parsed().pathname = str; + return clone; + } + getQueryString() { + return this.parsed().query; + } + queryString(str) { + const clone = this.clone(); + clone.parsed().query = str; + return clone; + } + getRawPath() { + return this.parsed().pathname; + } + rawPath(input) { + const args = Array.isArray(input) ? input.flat(5) : [input]; + let str = args.join("/"); + str = str.replace(/\/+/g, "/"); // remove double slashes + str = str.replace(/\/$/, ""); // remove all trailing slashes + if (str[0] !== "/") { + str = "/" + str; + } + this.parsed().pathname = str; + return this; + } + encode(str) { + try { + return encodeURIComponent(str); + } + catch (ex) { + return new URLSearchParams({ "": str }).toString().slice(1); + } + } + decode(str) { + return decode(str); + } + clone() { + return new UrlGrey(this.toString()); + } + getParent() { + const pieces = this.getPath().split("/"); + let popped = pieces.pop(); + if (popped === "") { + // ignore trailing slash + popped = pieces.pop(); + } + if (!popped) { + throw new Error("The current path has no parent path"); + } + return this.clearQuery().hash("").path(pieces.join("/")); + } + getRawChild() { + const pieces = this.getPath().split("/"); + let last = lastItem(pieces); + if (pieces.length > 1 && last === "") { + // ignore trailing slashes + pieces.pop(); + last = lastItem(pieces); + } + return last; + } + rawChild(suffixes) { + return this.clearQuery() + .hash("") + .rawPath([this.getPath(), suffixes].flat(5)); + } + getChild() { + const pieces = pathPieces(this.getPath()); + let last = lastItem(pieces); + if (pieces.length > 1 && last === "") { + // ignore trailing slashes + pieces.pop(); + last = lastItem(pieces); + } + return last; + } + child(suffixes) { + return this.clearQuery().hash("").path([this.getPath(), suffixes].flat(5)); + } + toJSON() { + return this.toString(); + } + toString() { + const p = this.parsed(); + let retval = p.protocol + "://"; + if (this.getProtocol() !== "file") { + const userinfo = p.username + ":" + p.password; + if (userinfo !== ":") { + retval += userinfo + "@"; + } + retval += p.hostname; + const port = portString(this); + if (port) { + retval += ":" + port; + } + } + retval += this.getPath() === "/" ? "" : this.getPath(); + const qs = p.query; + if (qs) { + retval += "?" + qs; + } + if (p.hash) { + retval += "#" + p.hash; + } + return retval; + } +} +exports.UrlGrey = UrlGrey; +_UrlGrey_url = new WeakMap(); +const pathPieces = function (path) { + const pieces = path.split("/"); + for (let i = 0; i < pieces.length; i++) { + pieces[i] = decode(pieces[i]); + } + return pieces; +}; +const decode = function (str) { + try { + return decodeURIComponent(str); + } + catch (ex) { + const val = new URLSearchParams(`=${str}`).get(""); + if (!val && val !== "") { + throw new Error(`decode() input could not be decoded: ${str}`); + } + return val; + } +}; +const portString = function (o) { + const port = o.getPort(); + if (o.getProtocol() === "https") { + if (port === 443) { + return ""; + } + } + if (o.getProtocol() === "http") { + if (port === 80) { + return ""; + } + } + if (port || port === 0) + return `${port}`; + return ""; +}; +const qsParse = (qsStr) => { + const params = new URLSearchParams(qsStr); + const retval = {}; + for (const param of params) { + retval[param[0]] = param[1]; + } + return retval; +}; +/* +UrlGrey.prototype.absolute = function(path){ + if (path[0] == '/'){ + path = path.substring(1); + } + var parsed = urlParse(path); + if (!!parsed.protocol){ // if it's already absolute, just return it + return path; + } + return this._protocol + "://" + this._host + '/' + path; +}; + +// TODO make this interpolate vars into the url. both sinatra style and url-tempates +// TODO name this: +UrlGrey.prototype.get = function(nameOrPath, varDict){ + if (!!nameOrPath){ + if (!!varDict){ + return this.absolute(this._router.getUrl(nameOrPath, varDict)); + } + return this.absolute(this._router.getUrl(nameOrPath)); + } + return this.url; +};*/ +/* +// TODO needs to take a template as an input +UrlGrey.prototype.param = function(key, defaultValue){ + var value = this.params()[key]; + if (!!value) { + return value; + } + return defaultValue; +}; + +// TODO extract params, given a template? +// TODO needs to take a template as an input +UrlGrey.prototype.params = function(inUrl){ + if (!!inUrl){ + return this._router.pathVariables(inUrl); + } + if (!!this._params){ + return this._params; + } + return this._router.pathVariables(this.url); +}; +*/ +// TODO relative() // takes an absolutepath and returns a relative one +// TODO absolute() // takes a relative path and returns an absolute one. +function urlgrey(url) { + return new UrlGrey(url); +} +exports.default = urlgrey; +urlgrey.UrlGrey = UrlGrey; +// function addPropertyGetterSetter(propertyName: string, methodName?: string) { +// if (!methodName) { +// methodName = propertyName; +// } +// UrlGrey.prototype[methodName] = function (str) { +// if (!str && str !== "") { +// return this.parsed()[propertyName]; +// } +// var obj = new UrlGrey(this.toString()); +// obj.parsed()[propertyName] = str; +// return obj; +// }; +// } diff --git a/package.json b/package.json index 87136af..012f225 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url", "uri" ], - "version": "2.0.0", + "version": "2.1.0", "bugs": { "url": "https://github.com/cainus/urlgrey/issues" }, @@ -28,7 +28,7 @@ "engine": { "node": ">=14.0.0" }, - "main": "index.js", + "main": "lib/index.js", "repository": { "type": "git", "url": "git://github.com/cainus/urlgrey.git" diff --git a/tsconfig.json b/tsconfig.json index 75dcaea..0731be6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,7 +49,7 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./lib", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -99,5 +99,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": ["./index.ts"], + "exclude": ["node_modules", "__tests__"] }