Import from Nibriboard-Panel

This commit is contained in:
Starbeamrainbowlabs 2019-04-26 23:35:10 +01:00
commit 2fe1f30fad
Signed by: sbrl
GPG Key ID: 1BE5172E637709C2
9 changed files with 592 additions and 0 deletions

91
.gitignore vendored Normal file
View File

@ -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

29
.tern-project Normal file
View File

@ -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": {}
}
}

105
Client/Router.mjs Normal file
View File

@ -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;

169
Server/Router.mjs Normal file
View File

@ -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;

73
Server/RouterContext.mjs Normal file
View File

@ -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;

56
Server/Sender.mjs Normal file
View File

@ -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;

7
index.mjs Normal file
View File

@ -0,0 +1,7 @@
"use strict";
import Router as ServerRouter from './Server/Router.mjs';
export {
ServerRouter
};

33
package-lock.json generated Normal file
View File

@ -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"
}
}
}
}

29
package.json Normal file
View File

@ -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"
}
}