commit 2fe1f30fad014ceed09bcd2e5d3cfa1d5ac1e91a
Author: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date:   Fri Apr 26 23:35:10 2019 +0100

    Import from Nibriboard-Panel

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e43a726
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,91 @@
+
+# Created by https://www.gitignore.io/api/node
+# Edit at https://www.gitignore.io/?templates=node
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# next.js build output
+.next
+
+# nuxt.js build output
+.nuxt
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# End of https://www.gitignore.io/api/node
diff --git a/.tern-project b/.tern-project
new file mode 100644
index 0000000..b556d9c
--- /dev/null
+++ b/.tern-project
@@ -0,0 +1,29 @@
+{
+  "ecmaVersion": 8,
+  "libs": [],
+  "loadEagerly": [
+    "**.mjs"
+  ],
+  "dontLoad": [],
+  "plugins": {
+    "doc_comment": true,
+    "node": {
+      "dontLoad": "",
+      "load": "",
+      "modules": ""
+    },
+    "node_resolve": {},
+    "modules": {
+      "dontLoad": "",
+      "load": "",
+      "modules": ""
+    },
+    "es_modules": {},
+    "requirejs": {
+      "baseURL": "",
+      "paths": "",
+      "override": ""
+    },
+    "commonjs": {}
+  }
+}
\ No newline at end of file
diff --git a/Client/Router.mjs b/Client/Router.mjs
new file mode 100644
index 0000000..214cc50
--- /dev/null
+++ b/Client/Router.mjs
@@ -0,0 +1,105 @@
+"use strict";
+
+import EventEmitter from 'event-emitter-es6';
+
+class ClientRouter extends EventEmitter {
+	constructor(options) {
+		super();
+		
+		/***** Settings *****/
+		this.listen_pushstate = true;
+		this.verbose = false;
+		
+		/********************/
+		
+		for(let key in options) {
+			this[key] = options[key];
+		}
+		
+		/********************/
+		
+		/** Whether we should handle popstate events or not. @type {Boolean} */
+		this.handle_popstates = true;
+		this.routes = [];
+		
+		window.addEventListener("popstate", ((event) => {
+			if(!this.handle_popstates) return;
+			
+		    console.info(`[ClientRouter] Handling popstate - path: ${window.location.hash.substr(1)} (state`, event.state, `)`);
+			this.navigate(window.location.hash.substr(1));
+		}).bind(this));
+	}
+	
+	add_404(callback) {
+		this.callback_404 = callback;
+	}
+	
+	add_page(routespec, callback) {
+		this.routes.push({
+			spec: routespec,
+			match: this.pathspec_to_regex(routespec),
+			callback
+		});
+	}
+	
+	navigate(path) {
+		for(let route_info of this.routes) {
+			const matches = path.match(route_info.match.regex);
+			if(matches) {
+				if(this.verbose) console.log(`%c[Router] Matched against ${route_info.match.regex}!`, "color: hsl(331, 76%, 40%); font-weight: bold;");
+				
+				// Build the parameters object
+				let params = {};
+				for(let i = 1; i < matches.length; i++) { // Skip the top-level group match
+					params[route_info.match.tokens[i-1]] = matches[i];
+				}
+				
+				// Don't handle any popstates potentially generated by changing the hash
+				this.handle_popstates = false;
+				window.location.hash = `#${path}`;
+				this.handle_popstates = true;
+				
+				route_info.callback(params);
+				return;
+			}
+			
+			if(this.verbose) console.debug(`%c[Router] No match against ${route_info.match.regex} - moving on.`, "color: hsl(331, 76%, 40%)");
+		}
+		
+		if(this.verbose) console.warn(`Couldn't find a match for '${path}'.`);
+		if(typeof this.callback_404 == "function")
+			this.callback_404(path);
+	}
+	
+	navigate_current_hash() {
+		this.navigate(window.location.hash.substr(1));
+	}
+	
+	/**
+	 * Converts a path specification into a regular expression.
+	 * From the server-side sibling of this (client-side) router.
+	 * @param	{string}	pathspec	The path specification to convert.
+	 * @return	{RegExp}	The resulting regular expression
+	 */
+	pathspec_to_regex(pathspec) {
+		if(pathspec == "*") // Support wildcards
+			return { regex: /^/, tokens: [] };
+		
+		let tokens = [];
+		let regex = new RegExp("^" + pathspec.replace(/::?([a-zA-Z0-9\-_]+)/g, (substr/*, index, template (not used)*/) => {
+			tokens.push(substr.replace(/:/g, ""));
+			
+			// FUTURE: We could add optional param support here too
+			if(substr.startsWith("::"))
+				return `(.+)`;
+			else
+				return `([^\/]+)`; 
+		}) + "$", "i");
+		
+		if(this.verbose) console.info("[router/verbose] Created regex", regex);
+		
+		return { regex, tokens };
+	}
+}
+
+export default ClientRouter;
diff --git a/Server/Router.mjs b/Server/Router.mjs
new file mode 100644
index 0000000..147f12a
--- /dev/null
+++ b/Server/Router.mjs
@@ -0,0 +1,169 @@
+"use strict";
+
+import RouterContext from './RouterContext.mjs';
+
+/**
+ * A standalone HTTP router that's based on the principle of middleware.
+ * Based on rill (see the npm package bearing the name), but stripped down and 
+ * simplified.
+ */
+class Router
+{
+	constructor(verbose = false) {
+		/** The actions to run in turn. */
+		this.actions = [];
+		/** Whether to activate versbose mode. Useful for debugging the router. */
+		this.verbose = verbose;
+		
+		this.default_action = async (ctx) => {
+			let message = `No route was found for '${ctx.request.url}'.`;
+			if(this.verbose) {
+				message += `\n\nRegistered Actions:\n`;
+				for(let action of this.actions) {
+					message += ` - ${action.toString()}\n`;
+				}
+			}
+			ctx.send.plain(404, message);
+		}
+	}
+	
+	/** Shortcut function for attaching an action to any request method. Full function: on */
+	any(pathspec, action) { this.on("*", pathspec, action); }
+	/** Shortcut function for attaching an action to head requests. Full function: on */
+	head(pathspec, action) { this.on(["head"], pathspec, action); }
+	/** Shortcut function for attaching an action to get requests. Full function: on */
+	get(pathspec, action) { this.on(["get"], pathspec, action); }
+	/** Shortcut function for attaching an action to post requests. Full function: on */
+	post(pathspec, action) { this.on(["post"], pathspec, action); }
+	/** Shortcut function for attaching an action to put requests. Full function: on */
+	put(pathspec, action) { this.on(["put"], pathspec, action); }
+	/** Shortcut function for attaching an action to delete requests. Full function: on */
+	delete(pathspec, action) { this.on(["delete"], pathspec, action); }
+	/** Shortcut function for attaching an action to options requests. Full function: on */
+	options(pathspec, action) { this.on(["options"], pathspec, action); }
+	
+	/**
+	 * Execute the specified action for requests matching the given parameters.
+	 * TODO: Consider merging with on_all and refactoring to take an object literal which we cna destructure instead
+	 * @param	{array}			methods		The HTTP methods to run the action for. Include '*' to specify all methods.
+	 * @param	{string|regex}	pathspec	The specification of the paths that this action should match against. May be a regular expression.
+	 * @param	{Function}		action		The action to execute. Will be passed the parameters `context` (Object) and `next` (Function).
+	 */
+	on(methods, pathspec, action) {
+		let regex_info = pathspec instanceof RegExp ? {regex: pathspec, tokens: [] } : this.pathspec_to_regex(pathspec);
+		
+		// next must be a generator that returns each action in turn
+		this.actions.push(async (context, next) => {
+			const matches = context.url.pathname.match(regex_info.regex);
+			
+			if(this.verbose) console.error(`[router/verbose] [${methods.join(", ")} -> ${pathspec} ] Matches: `, matches);
+			
+			if((methods.indexOf(context.request.method.toLowerCase()) > -1 || methods.indexOf("*") > -1) && matches) {
+				if(this.verbose) console.error(`[router/verbose] Match found! Executing action.`);
+				for(let i = 1; i < matches.length; i++) { // Skip the top-level group
+					context.params[regex_info.tokens[i-1]] = matches[i];
+				}
+				await action(context, next);
+			}
+			else {
+				if(this.verbose) console.error(`[router/verbose] Nope, didn't match. Moving on`);
+				await next();
+			}
+		});
+	}
+	
+	on_all(action) {
+		this.actions.push(action);
+	}
+	
+	/**
+	 * Runs the specified action for requests if the provided testing function 
+	 * returns true.
+	 * @param  {Function} test   The testing function. Will be passed the context as it's only parameter.
+	 * @param  {Function} action The action to run if the test returns true.
+	 */
+	onif(test, action) {
+		this.actions.push(async (context, next) => {
+			let test_result = test(context);
+			if(this.verbose) console.error("[router/verbose] Test action result: ", test_result);
+			if(test_result)
+				await action(context, next);
+			else
+				await next(context);
+		})
+	}
+	
+	/**
+	 * Converts a path specification into a regular expression.
+	 * @param	{string}	pathspec	The path specification to convert.
+	 * @return	{RegExp}	The resulting regular expression
+	 */
+	pathspec_to_regex(pathspec) {
+		if(pathspec == "*") // Support wildcards
+			return { regex: /^/, tokens: [] };
+		
+		let tokens = [];
+		let regex = new RegExp("^" + pathspec.replace(/::?([a-zA-Z0-9\-_]+)/g, (substr/*, index, template (not actually used)*/) => {
+			tokens.push(substr.replace(/:/g, ""));
+			
+			// FUTURE: We could add optional param support here too
+			if(substr.startsWith("::"))
+				return `(.+)`;
+			else
+				return `([^\/]+)`; 
+		}) + "$", "i");
+		
+		/*if(this.verbose)*/ console.error("[router/verbose] Created regex", regex);
+		
+		return { regex, tokens };
+	}
+	
+	/**
+	 * Handles the specified request.
+	 * @param  {http.ClientRequest} request  The request to handle.
+	 * @param  {http.ServerResponse} response The response object to use to send the response.
+	 * @return {[type]}          [description]
+	 */
+	async handle(request, response) {
+		let context = new RouterContext(request, response),
+			iterator = this.iterate();
+		
+		// Begin the middleware execution
+		this.gen_next(iterator, context)();
+	}
+	
+	/**
+	 * Returns an anonymous function that, when called, will execute the next
+	 * item of middleware.
+	 * It achieves this via a combination of a generator, anonymous function
+	 * scope abuse, being recursive, and magic.
+	 * @param	 {Generator}	iterator	The generator that emits the middleware.
+	 * @param	{Object}		context		The context of the request.
+	 * @return	{Function}					A magic next function.
+	 */
+	gen_next(iterator, context) {
+		let next_raw = iterator.next();
+		// Return the default action if we've reached the end of the line
+		if(next_raw.done)
+			return async () => { 
+				this.default_action(context);
+			};
+		
+		return (async () => {
+			if(this.verbose) console.error(`[router/verbose] Executing ${next_raw.value}`);
+			await next_raw.value(context, this.gen_next(iterator, context));
+		}).bind(this); // Don't forget to bind each successive function to this context
+	}
+	
+	/**
+	 * Iterates over all the generated middleware.
+	 * @return {Generator} A generator that returns each successive piece of middleware in turn.
+	 */
+	*iterate() {
+		for(let action of this.actions) {
+			yield action;
+		}
+	}
+}
+
+export default Router;
diff --git a/Server/RouterContext.mjs b/Server/RouterContext.mjs
new file mode 100644
index 0000000..0adca47
--- /dev/null
+++ b/Server/RouterContext.mjs
@@ -0,0 +1,73 @@
+"use strict";
+
+// core
+import url from 'url';
+// npm
+import cookie from 'cookie';
+// files
+import Sender from './Sender.mjs';
+
+/**
+ * Contains context information about a single request / response pair.
+ */
+class RouterContext {
+	constructor(in_request, in_response) {
+		/**
+		 * The Node.JS request object 
+		 * @type	{http.ClientRequest}
+		 */
+		this.request = in_request;
+		/**
+		 * The Node.JS response object
+		 * @type	{http.ServerResponse}
+		 */
+		this.response = in_response;
+		/**
+		 * The parsed request URL
+		 * @type	{URL}
+		 */
+		this.url = url.parse(this.request.url, true);
+		/**
+		 * The url parameters parsed out by the router
+		 * @type	{Object}
+		 */
+		this.params = {};
+		/**
+		 * An object containing some utitlity methods for quickly sending responses
+		 * @type	{Sender}
+		 */
+		this.send = new Sender(this.response);
+		
+		// FUTURE: Refactor the default population of this object elsewhere
+		/**
+		 * The environment object.
+		 * State variables that need to be attached to a specific request can 
+		 * go in here.
+		 * @type	{Object}
+		 */
+		this.env = {
+			/**
+			 * Whether the user is logged in or not.
+			 * @type {Boolean}
+			 */
+			logged_in: false,
+			/**
+			 * The user's name. Guaranteed to be specified - if only as "anonymous".
+			 * @type {String}
+			 */
+			username: "anonymous",
+			/**
+			 * The parsed cookie object
+			 * @type {Object}
+			 */
+			cookie: cookie.parse(this.request.headers["cookie"] || ""),
+			/**
+			 * The parsed post data as an object, if applicable.
+			 * @type {Object|null}
+			 */
+			post_data: null
+		};
+	}
+}
+
+export default RouterContext;
diff --git a/Server/Sender.mjs b/Server/Sender.mjs
new file mode 100644
index 0000000..d4bc97c
--- /dev/null
+++ b/Server/Sender.mjs
@@ -0,0 +1,56 @@
+"use strict";
+
+import { NightInkFile } from 'nightink';
+
+class Sender {
+	constructor(response) {
+		this.response = response;
+	}
+	
+	/**
+	 * Sends a HTML response, rendering a NightInk template.
+	 * Don't forget to await this!
+	 * @param  {number}  status_code       The status code to return.
+	 * @param  {string}  template_filename The path to the filename containing the template to render.
+	 * @param  {Object}  data              The data to use whilst rendering the template.
+	 * @return {Promise}
+	 */
+	async html(status_code, template_filename, data) {
+		let response_html = await NightInkFile(template_filename, data);
+		this.response.writeHead(status_code, {
+			"content-type": "text/html",
+			"content-length": Buffer.byteLength(response_html, "utf8"),
+		});
+		this.response.end(response_html);
+	}
+	
+	/**
+	 * Sends a plain text response.
+	 * @param  {number} status_code The HTTP status code to return.
+	 * @param  {string} data        The data to send.
+	 */
+	plain(status_code, data) {
+		this.response.writeHead(status_code, {
+			"content-type": "text/plain",
+			"content-length": Buffer.byteLength(data, "utf8"),
+		});
+		this.response.end(data);
+	}
+	
+	/**
+	 * Sends a redirect.
+	 * @param  {number} status_code The HTTP status code to send.
+	 * @param  {string} new_path    The (possibly relative) uri to redirect the client to.
+	 * @param  {string} message     The informational plain-text message to return, just in case.
+	 */
+	redirect(status_code, new_path, message) {
+		this.response.writeHead(status_code, {
+			"location": new_path,
+			"content-type": "text/plain",
+			"content-length": message.length
+		});
+		this.response.end(message);
+	}
+}
+
+export default Sender;
diff --git a/index.mjs b/index.mjs
new file mode 100644
index 0000000..8d2c904
--- /dev/null
+++ b/index.mjs
@@ -0,0 +1,7 @@
+"use strict";
+
+import Router as ServerRouter from './Server/Router.mjs';
+
+export {
+	ServerRouter
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..284fb08
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,33 @@
+{
+  "name": "powerroot",
+  "version": "1.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "await-fs": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/await-fs/-/await-fs-1.0.0.tgz",
+      "integrity": "sha1-QAnTAIYz/WYlqgCfCm8aujY38wE="
+    },
+    "event-emitter-es6": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/event-emitter-es6/-/event-emitter-es6-1.1.5.tgz",
+      "integrity": "sha1-75UxGy4Xqjm+djsDHOSvfunLeEk=",
+      "dev": true
+    },
+    "html-entities": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
+      "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8="
+    },
+    "nightink": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/nightink/-/nightink-0.1.2.tgz",
+      "integrity": "sha512-nAyyf1EvghaFtmwD4ox2rFiY0eWqAsW6rulIPaPC5IMv3YaNKYdQ4F7zUZ6E7eONEVQGtiCGTv6YksY9wHHs1g==",
+      "requires": {
+        "await-fs": "^1.0.0",
+        "html-entities": "^1.2.1"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f1116eb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+  "name": "powerroot",
+  "version": "1.0.0",
+  "description": "Client and server-side routing micro frameworks",
+  "main": "index.mjs",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/sbrl/powerroot.git"
+  },
+  "keywords": [
+    "routing",
+    "micro-framework"
+  ],
+  "author": "Starbeamrainbowlabs",
+  "license": "MPL-2.0",
+  "bugs": {
+    "url": "https://github.com/sbrl/powerroot/issues"
+  },
+  "homepage": "https://github.com/sbrl/powerroot#readme",
+  "dependencies": {
+    "nightink": "^0.1.2"
+  },
+  "devDependencies": {
+    "event-emitter-es6": "^1.1.5"
+  }
+}
