things+stuff
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
export function withSides(sides) {
|
||||
export function dice(sides) {
|
||||
const r = Math.random();
|
||||
return Math.floor(r * sides) + 1;
|
||||
}
|
||||
|
||||
export function d6() {
|
||||
return withSides(6);
|
||||
return dice(6);
|
||||
}
|
||||
|
||||
export function d8() {
|
||||
return withSides(8);
|
||||
return dice(8);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,52 @@
|
||||
export function cleanName(s) {
|
||||
if (typeof s !== "string") {
|
||||
throw new Error("String expected, but got a ", typeof s);
|
||||
const UID_DIGITS = 12;
|
||||
const MINI_UID_REGEX = /\.uid\.[a-z0-9]{6,}$/;
|
||||
const ID_SANITY_REGEX = /^:([a-z0-9]+\.)*[a-z0-9_]+$/;
|
||||
|
||||
/**
|
||||
* Sanity check a string to see if it is a potential id.
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIdSane(id) {
|
||||
if (typeof id !== "string") {
|
||||
return false;
|
||||
}
|
||||
return s
|
||||
.toLowerCase()
|
||||
.replace(" ", "_")
|
||||
.replace(/[^a-zA-Z0-9_]/, "_");
|
||||
|
||||
if (id.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ID_SANITY_REGEX.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} crypto-unsafe pseudo random number.
|
||||
*
|
||||
* Generate a random number, convert it to base36, and return it as a string with 7-8 characters.
|
||||
*/
|
||||
export function miniUid() {
|
||||
// we use 12 digits, but we could go up to 16
|
||||
return Number(Math.random().toFixed(12).substring(2)).toString(36);
|
||||
return Number(Math.random().toFixed(UID_DIGITS).substring(2)).toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an id from a name
|
||||
* Generate an id from a string
|
||||
* @param {string[]} str
|
||||
*/
|
||||
export function fromName(...names) {
|
||||
let res = "";
|
||||
for (const name of names) {
|
||||
res += ":" + cleanName(name);
|
||||
}
|
||||
|
||||
return res + ":" + miniUid();
|
||||
export function appendMiniUid(str) {
|
||||
return str + ".uid." + miniUid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a given string end with ".uid.23khtasdz", etc.
|
||||
*
|
||||
* @param {string} str
|
||||
*/
|
||||
export function endsWithMiniUid(str) {
|
||||
return MINI_UID_REGEX.test(str);
|
||||
}
|
||||
|
||||
export function appendOrReplaceMiniUid(str) {
|
||||
return appendMiniUid(str.replace(MINI_UID_REGEX, ""));
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
*/
|
||||
export const CALAMITY = "calamity";
|
||||
|
||||
/**
|
||||
* Tell recipient that an error has occurred
|
||||
/**
|
||||
* Tell recipient that an error has occurred
|
||||
*
|
||||
* Server-->Client-->Player
|
||||
*/
|
||||
@@ -21,7 +21,6 @@ export const ERROR = "e";
|
||||
*/
|
||||
export const MESSAGE = "m";
|
||||
|
||||
|
||||
/**
|
||||
* Player has entered data, and sends it to server.
|
||||
*
|
||||
@@ -77,110 +76,126 @@ export const DEBUG = "dbg";
|
||||
* Represents a message sent from client to server.
|
||||
*/
|
||||
export class ClientMessage {
|
||||
/**
|
||||
* @protected
|
||||
* @type {any[]} _arr The array that contains the message data
|
||||
*/
|
||||
_attr;
|
||||
/**
|
||||
* @protected
|
||||
* @type {any[]} _arr The array that contains the message data
|
||||
*/
|
||||
_attr;
|
||||
|
||||
/** The message type.
|
||||
*
|
||||
* One of the * constants from this document.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get type() {
|
||||
return this._attr[0];
|
||||
/** The message type.
|
||||
*
|
||||
* One of the * constants from this document.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get type() {
|
||||
return this._attr[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} msgData the raw text data in the websocket message.
|
||||
*/
|
||||
constructor(msgData) {
|
||||
if (typeof msgData !== "string") {
|
||||
throw new Error(
|
||||
"Could not create client message. Attempting to parse json, but data was not even a string, it was a " +
|
||||
typeof msgData,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} msgData the raw text data in the websocket message.
|
||||
*/
|
||||
constructor(msgData) {
|
||||
if (typeof msgData !== "string") {
|
||||
throw new Error("Could not create client message. Attempting to parse json, but data was not even a string, it was a " + typeof msgData);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._attr = JSON.parse(msgData);
|
||||
} catch (_) {
|
||||
throw new Error(`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(this._attr)) {
|
||||
throw new Error(`Could not create client message. Excpected an array, but got a ${typeof this._attr}`);
|
||||
}
|
||||
|
||||
if (this._attr.length < 1) {
|
||||
throw new Error("Could not create client message. Excpected an array with at least 1 element, but got an empty one");
|
||||
}
|
||||
}
|
||||
hasCommand() {
|
||||
return this._attr.length > 1 && this._attr[0] === COMMAND;
|
||||
try {
|
||||
this._attr = JSON.parse(msgData);
|
||||
} catch (_) {
|
||||
throw new Error(
|
||||
`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Does this message contain a username-response from the client? */
|
||||
isUsernameResponse() {
|
||||
return this._attr.length === 4
|
||||
&& this._attr[0] === REPLY
|
||||
&& this._attr[1] === "username"
|
||||
&& typeof this._attr[2] === "string";
|
||||
if (!Array.isArray(this._attr)) {
|
||||
throw new Error(
|
||||
`Could not create client message. Excpected an array, but got a ${typeof this._attr}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Does this message contain a password-response from the client? */
|
||||
isPasswordResponse() {
|
||||
return this._attr.length === 4
|
||||
&& this._attr[0] === REPLY
|
||||
&& this._attr[1] === "password"
|
||||
&& typeof this._attr[2] === "string";
|
||||
if (this._attr.length < 1) {
|
||||
throw new Error(
|
||||
"Could not create client message. Excpected an array with at least 1 element, but got an empty one",
|
||||
);
|
||||
}
|
||||
}
|
||||
hasCommand() {
|
||||
return this._attr.length > 1 && this._attr[0] === COMMAND;
|
||||
}
|
||||
|
||||
/** Does this message contain a username-response from the client? */
|
||||
isUsernameResponse() {
|
||||
return (
|
||||
this._attr.length === 4 &&
|
||||
this._attr[0] === REPLY &&
|
||||
this._attr[1] === "username" &&
|
||||
typeof this._attr[2] === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/** Does this message contain a password-response from the client? */
|
||||
isPasswordResponse() {
|
||||
return (
|
||||
this._attr.length === 4 &&
|
||||
this._attr[0] === REPLY &&
|
||||
this._attr[1] === "password" &&
|
||||
typeof this._attr[2] === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/** @returns {boolean} does this message indicate the player wants to quit */
|
||||
isQuitCommand() {
|
||||
return this._attr[0] === QUIT;
|
||||
}
|
||||
|
||||
isHelpCommand() {
|
||||
return this._attr[0] === HELP;
|
||||
}
|
||||
|
||||
/** @returns {boolean} is this a debug message? */
|
||||
isDebug() {
|
||||
return this._attr.length === 2 && this._attr[0] === DEBUG;
|
||||
}
|
||||
|
||||
isIntegerResponse() {
|
||||
return (
|
||||
this._attr.length === 4 &&
|
||||
this._attr[0] === REPLY &&
|
||||
this._attr[1] === "integer" &&
|
||||
(typeof this._attr[2] === "string" ||
|
||||
typeof this._attr[2] === "number") &&
|
||||
Number.isInteger(Number(this._attr[2]))
|
||||
);
|
||||
}
|
||||
|
||||
/** @returns {number} integer */
|
||||
get integer() {
|
||||
if (!this.isIntegerResponse()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** @returns {boolean} does this message indicate the player wants to quit */
|
||||
isQuitCommand() {
|
||||
return this._attr[0] === QUIT
|
||||
}
|
||||
return Number.parseInt(this._attr[2]);
|
||||
}
|
||||
|
||||
isHelpCommand() {
|
||||
return this._attr[0] === HELP
|
||||
}
|
||||
/** @returns {string|false} Get the username stored in this message */
|
||||
get username() {
|
||||
return this.isUsernameResponse() ? this._attr[2] : false;
|
||||
}
|
||||
|
||||
/** @returns {boolean} is this a debug message? */
|
||||
isDebug() {
|
||||
return this._attr.length === 2 && this._attr[0] === DEBUG;
|
||||
}
|
||||
/** @returns {string|false} Get the password stored in this message */
|
||||
get password() {
|
||||
return this.isPasswordResponse() ? this._attr[2] : false;
|
||||
}
|
||||
|
||||
isIntegerResponse() {
|
||||
return this._attr.length === 4
|
||||
&& this._attr[0] === REPLY
|
||||
&& this._attr[1] === "integer"
|
||||
&& (typeof this._attr[2] === "string" || typeof this._attr[2] === "number")
|
||||
&& Number.isInteger(Number(this._attr[2]));
|
||||
}
|
||||
|
||||
/** @returns {number} integer */
|
||||
get integer() {
|
||||
if (!this.isIntegerResponse()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Number.parseInt(this._attr[2]);
|
||||
}
|
||||
|
||||
/** @returns {string|false} Get the username stored in this message */
|
||||
get username() {
|
||||
return this.isUsernameResponse() ? this._attr[2] : false;
|
||||
}
|
||||
|
||||
/** @returns {string|false} Get the password stored in this message */
|
||||
get password() {
|
||||
return this.isPasswordResponse() ? this._attr[2] : false;
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
get command() {
|
||||
return this.hasCommand() ? this._attr[1] : false;
|
||||
}
|
||||
/** @returns {string} */
|
||||
get command() {
|
||||
return this.hasCommand() ? this._attr[1] : false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,5 +205,5 @@ export class ClientMessage {
|
||||
* @param {...any} args
|
||||
*/
|
||||
export function prepare(messageType, ...args) {
|
||||
return JSON.stringify([messageType, ...args]);
|
||||
return JSON.stringify([messageType, ...args]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { randomBytes, pbkdf2Sync } from "node:crypto";
|
||||
import { Config } from "../config.js";
|
||||
|
||||
|
||||
// Settings (tune as needed)
|
||||
const ITERATIONS = 1000;
|
||||
const KEYLEN = 32; // 32-bit hash
|
||||
@@ -14,9 +13,11 @@ const DEV = process.env.NODE_ENV === "dev";
|
||||
* @returns {string}
|
||||
*/
|
||||
export function generateHash(password) {
|
||||
const salt = randomBytes(16).toString("hex"); // 128-bit salt
|
||||
const hash = pbkdf2Sync(password, salt, ITERATIONS, KEYLEN, DIGEST).toString("hex");
|
||||
return `${ITERATIONS}:${salt}:${hash}`;
|
||||
const salt = randomBytes(16).toString("hex"); // 128-bit salt
|
||||
const hash = pbkdf2Sync(password, salt, ITERATIONS, KEYLEN, DIGEST).toString(
|
||||
"hex",
|
||||
);
|
||||
return `${ITERATIONS}:${salt}:${hash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,35 +28,41 @@ export function generateHash(password) {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function verifyPassword(password_candidate, stored_password_hash) {
|
||||
const [iterations, salt, hash] = stored_password_hash.split(":");
|
||||
const derived = pbkdf2Sync(password_candidate, salt, Number(iterations), KEYLEN, DIGEST).toString("hex");
|
||||
const success = hash === derived;
|
||||
if (Config.dev || true) {
|
||||
console.debug(
|
||||
"Verifying password:\n" +
|
||||
" Input : %s (the password as it was sent to us by the client)\n" +
|
||||
" Given : %s (the input password hashed by us (not necessary for validation))\n" +
|
||||
" Stored : %s (the password hash we have on file for the player)\n" +
|
||||
" Derived : %s (the hashed version of the input password)\n" +
|
||||
" Verified : %s (was the password valid)",
|
||||
password_candidate,
|
||||
generateHash(password_candidate),
|
||||
stored_password_hash,
|
||||
derived,
|
||||
success,
|
||||
);
|
||||
}
|
||||
return success;
|
||||
const [iterations, salt, hash] = stored_password_hash.split(":");
|
||||
const derived = pbkdf2Sync(
|
||||
password_candidate,
|
||||
salt,
|
||||
Number(iterations),
|
||||
KEYLEN,
|
||||
DIGEST,
|
||||
).toString("hex");
|
||||
const success = hash === derived;
|
||||
if (Config.dev || true) {
|
||||
console.debug(
|
||||
"Verifying password:\n" +
|
||||
" Input : %s (the password as it was sent to us by the client)\n" +
|
||||
" Given : %s (the input password hashed by us (not necessary for validation))\n" +
|
||||
" Stored : %s (the password hash we have on file for the player)\n" +
|
||||
" Derived : %s (the hashed version of the input password)\n" +
|
||||
" Verified : %s (was the password valid)",
|
||||
password_candidate,
|
||||
generateHash(password_candidate),
|
||||
stored_password_hash,
|
||||
derived,
|
||||
success,
|
||||
);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/** @param {string} candidate */
|
||||
export function isUsernameSane(candidate) {
|
||||
return /^[a-zA-Z0-9_]{4,}$/.test(candidate);
|
||||
return /^[a-zA-Z0-9_]{4,}$/.test(candidate);
|
||||
}
|
||||
|
||||
/** @param {string} candidate */
|
||||
export function isPasswordSane(candidate) {
|
||||
// We know the password must adhere to one of our client-side-hashed crypto schemes,
|
||||
// so we can be fairly strict with the allowed passwords
|
||||
return /^[a-zA-Z0-9_: -]{8,}$/.test(candidate);
|
||||
// We know the password must adhere to one of our client-side-hashed crypto schemes,
|
||||
// so we can be fairly strict with the allowed passwords
|
||||
return /^[a-zA-Z0-9_: -]{8,}$/.test(candidate);
|
||||
}
|
||||
|
||||
@@ -4,136 +4,134 @@
|
||||
* @enum {string}
|
||||
*/
|
||||
export const FrameType = {
|
||||
/**
|
||||
* ╔════════════╗
|
||||
* ║ Hello, TUI ║
|
||||
* ╚════════════╝
|
||||
*
|
||||
* @type {string} Double-lined frame
|
||||
*/
|
||||
Double: "Double",
|
||||
|
||||
/**
|
||||
* ╔════════════╗
|
||||
* ║ Hello, TUI ║
|
||||
* ╚════════════╝
|
||||
*
|
||||
* @type {string} Double-lined frame
|
||||
*/
|
||||
Double: "Double",
|
||||
/**
|
||||
* ┌────────────┐
|
||||
* │ Hello, TUI │
|
||||
* └────────────┘
|
||||
*
|
||||
* @type {string} Single-lined frame
|
||||
*/
|
||||
Single: "Single",
|
||||
|
||||
/**
|
||||
* ┌────────────┐
|
||||
* │ Hello, TUI │
|
||||
* └────────────┘
|
||||
*
|
||||
* @type {string} Single-lined frame
|
||||
*/
|
||||
Single: "Single",
|
||||
/**
|
||||
*
|
||||
* Hello, TUI
|
||||
*
|
||||
*
|
||||
* @type {string} Double-lined frame
|
||||
*/
|
||||
Invisible: "Invisible",
|
||||
|
||||
/**
|
||||
* ( )
|
||||
* ( Hello, TUI )
|
||||
* ( )
|
||||
*
|
||||
* @type {string} Double-lined frame
|
||||
*/
|
||||
Parentheses: "Parentheses",
|
||||
|
||||
/**
|
||||
*
|
||||
* Hello, TUI
|
||||
*
|
||||
*
|
||||
* @type {string} Double-lined frame
|
||||
*/
|
||||
Invisible: "Invisible",
|
||||
/**
|
||||
* +------------+
|
||||
* | Hello, TUI |
|
||||
* +------------+
|
||||
*
|
||||
* @type {string} Double-lined frame
|
||||
*/
|
||||
Basic: "Basic",
|
||||
|
||||
|
||||
/**
|
||||
* ( )
|
||||
* ( Hello, TUI )
|
||||
* ( )
|
||||
*
|
||||
* @type {string} Double-lined frame
|
||||
*/
|
||||
Parentheses: "Parentheses",
|
||||
|
||||
/**
|
||||
* +------------+
|
||||
* | Hello, TUI |
|
||||
* +------------+
|
||||
*
|
||||
* @type {string} Double-lined frame
|
||||
*/
|
||||
Basic: "Basic",
|
||||
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* Default values for the common frame types.
|
||||
*
|
||||
* [north, south, east, west, northwest, northeast, southwest, southeast]
|
||||
*/
|
||||
values: {
|
||||
Basic: "--||++++",
|
||||
Double: "══║║╔╗╚╝",
|
||||
Invisible: " ",
|
||||
Parentheses: " () ",
|
||||
Single: "──││┌┐└┘",
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @protected
|
||||
* Default values for the common frame types.
|
||||
*
|
||||
* [north, south, east, west, northwest, northeast, southwest, southeast]
|
||||
*/
|
||||
values: {
|
||||
Basic: "--||++++",
|
||||
Double: "══║║╔╗╚╝",
|
||||
Invisible: " ",
|
||||
Parentheses: " () ",
|
||||
Single: "──││┌┐└┘",
|
||||
},
|
||||
};
|
||||
|
||||
export class FramingOptions {
|
||||
/** @type {number=0} Vertical Padding; number of vertical whitespace (newlines) between the text and the frame. */
|
||||
vPadding = 0;
|
||||
/** @type {number=0} Vertical Padding; number of vertical whitespace (newlines) between the text and the frame. */
|
||||
vPadding = 0;
|
||||
|
||||
/** @type {number=0} Margin ; number of newlines to to insert before and after the framed text */
|
||||
vMargin = 0;
|
||||
/** @type {number=0} Margin ; number of newlines to to insert before and after the framed text */
|
||||
vMargin = 0;
|
||||
|
||||
/** @type {number=0} Horizontal Padding; number of whitespace characters to insert between the text and the sides of the frame. */
|
||||
hPadding = 0;
|
||||
/** @type {number=0} Horizontal Padding; number of whitespace characters to insert between the text and the sides of the frame. */
|
||||
hPadding = 0;
|
||||
|
||||
/** @type {number=0} Margin ; number of newlines to to insert before and after the text, but inside the frame */
|
||||
hMargin = 0;
|
||||
/** @type {number=0} Margin ; number of newlines to to insert before and after the text, but inside the frame */
|
||||
hMargin = 0;
|
||||
|
||||
/** @type {FrameType=FrameType.Double} Type of frame to put around the text */
|
||||
frameType = FrameType.Double;
|
||||
/** @type {FrameType=FrameType.Double} Type of frame to put around the text */
|
||||
frameType = FrameType.Double;
|
||||
|
||||
/** @type {number=0} Pad each line to become at least this long */
|
||||
minLineWidth = 0;
|
||||
/** @type {number=0} Pad each line to become at least this long */
|
||||
minLineWidth = 0;
|
||||
|
||||
// Light block: ░ (U+2591)
|
||||
// Medium block: ▒ (U+2592)
|
||||
// Dark block: ▓ (U+2593)
|
||||
// Solid block: █ (U+2588)
|
||||
/** @type {string} Single character to use as filler inside the frame. */
|
||||
paddingChar = " "; // character used for padding inside the frame.
|
||||
// Light block: ░ (U+2591)
|
||||
// Medium block: ▒ (U+2592)
|
||||
// Dark block: ▓ (U+2593)
|
||||
// Solid block: █ (U+2588)
|
||||
/** @type {string} Single character to use as filler inside the frame. */
|
||||
paddingChar = " "; // character used for padding inside the frame.
|
||||
|
||||
/** @type {string} Single character to use as filler outside the frame. */
|
||||
marginChar = " ";
|
||||
/** @type {string} Single character to use as filler outside the frame. */
|
||||
marginChar = " ";
|
||||
|
||||
/** @type {string} The 8 characters that make up the frame elements */
|
||||
frameChars = FrameType.values.Double;
|
||||
/** @type {string} The 8 characters that make up the frame elements */
|
||||
frameChars = FrameType.values.Double;
|
||||
|
||||
/**
|
||||
* @param {object} o
|
||||
* @returns {FramingOptions}
|
||||
*/
|
||||
static fromObject(o) {
|
||||
const result = new FramingOptions();
|
||||
/**
|
||||
* @param {object} o
|
||||
* @returns {FramingOptions}
|
||||
*/
|
||||
static fromObject(o) {
|
||||
const result = new FramingOptions();
|
||||
|
||||
result.vPadding = Math.max(0, Number.parseInt(o.vPadding) || 0);
|
||||
result.hPadding = Math.max(0, Number.parseInt(o.hPadding) || 0);
|
||||
result.vMargin = Math.max(0, Number.parseInt(o.vMargin) || 0);
|
||||
result.hMargin = Math.max(0, Number.parseInt(o.hMargin) || 0);
|
||||
result.minLineWidth = Math.max(0, Number.parseInt(o.hMargin) || 0);
|
||||
result.vPadding = Math.max(0, Number.parseInt(o.vPadding) || 0);
|
||||
result.hPadding = Math.max(0, Number.parseInt(o.hPadding) || 0);
|
||||
result.vMargin = Math.max(0, Number.parseInt(o.vMargin) || 0);
|
||||
result.hMargin = Math.max(0, Number.parseInt(o.hMargin) || 0);
|
||||
result.minLineWidth = Math.max(0, Number.parseInt(o.hMargin) || 0);
|
||||
|
||||
result.paddingChar = String(o.paddingChar || " ")[0] || " ";
|
||||
result.marginChar = String(o.marginChar || " ")[0] || " ";
|
||||
result.paddingChar = String(o.paddingChar || " ")[0] || " ";
|
||||
result.marginChar = String(o.marginChar || " ")[0] || " ";
|
||||
|
||||
//
|
||||
// Do we have custom and valid frame chars?
|
||||
if (typeof o.frameChars === "string" && o.frameChars.length === FrameType.values.Double.length) {
|
||||
result.frameChars = o.frameChars;
|
||||
//
|
||||
// Do we have custom and valid frame chars?
|
||||
if (
|
||||
typeof o.frameChars === "string" &&
|
||||
o.frameChars.length === FrameType.values.Double.length
|
||||
) {
|
||||
result.frameChars = o.frameChars;
|
||||
|
||||
//
|
||||
// do we have document frame type instead ?
|
||||
} else if (o.frameType && FrameType.hasOwnProperty(o.frameType)) {
|
||||
result.frameChars = FrameType.values[o.frameType];
|
||||
//
|
||||
// do we have document frame type instead ?
|
||||
} else if (o.frameType && FrameType.hasOwnProperty(o.frameType)) {
|
||||
result.frameChars = FrameType.values[o.frameType];
|
||||
|
||||
// Fall back to using "Double" frame
|
||||
} else {
|
||||
result.frameChars = FrameType.values.Double;
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
// Fall back to using "Double" frame
|
||||
} else {
|
||||
result.frameChars = FrameType.values.Double;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,173 +139,198 @@ export class FramingOptions {
|
||||
* @param {FramingOptions} options
|
||||
*/
|
||||
export function frameText(text, options) {
|
||||
if (!options) {
|
||||
options = new FramingOptions();
|
||||
}
|
||||
|
||||
if (!options) {
|
||||
options = new FramingOptions();
|
||||
}
|
||||
if (!(options instanceof FramingOptions)) {
|
||||
options = FramingOptions.fromObject(options);
|
||||
}
|
||||
|
||||
if (!(options instanceof FramingOptions)) {
|
||||
options = FramingOptions.fromObject(options);
|
||||
}
|
||||
// There is a point to this; each element in the array may contain newlines,
|
||||
// so we have to combine everything into a long text and then split into
|
||||
// individual lines afterwards.
|
||||
if (Array.isArray(text)) {
|
||||
text = text.join("\n");
|
||||
}
|
||||
|
||||
// There is a point to this; each element in the array may contain newlines,
|
||||
// so we have to combine everything into a long text and then split into
|
||||
// individual lines afterwards.
|
||||
if (Array.isArray(text)) {
|
||||
text = text.join("\n");
|
||||
}
|
||||
if (typeof text !== "string") {
|
||||
console.debug(text);
|
||||
throw new Error(
|
||||
`text argument was neither an array or a string, it was a ${typeof text}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof text !== "string") {
|
||||
console.debug(text);
|
||||
throw new Error(`text argument was neither an array or a string, it was a ${typeof text}`);
|
||||
}
|
||||
/** @type {string[]} */
|
||||
const lines = text.split("\n");
|
||||
|
||||
/** @type {string[]} */
|
||||
const lines = text.split("\n");
|
||||
const innerLineLength = Math.max(
|
||||
lines.reduce((accumulator, currentLine) => {
|
||||
if (currentLine.length > accumulator) {
|
||||
return currentLine.length;
|
||||
}
|
||||
return accumulator;
|
||||
}, 0),
|
||||
options.minLineWidth,
|
||||
);
|
||||
|
||||
const innerLineLength = Math.max(
|
||||
lines.reduce((accumulator, currentLine) => {
|
||||
if (currentLine.length > accumulator) {
|
||||
return currentLine.length;
|
||||
}
|
||||
return accumulator;
|
||||
}, 0), options.minLineWidth);
|
||||
const frameThickness = 1; // always 1 for now.
|
||||
|
||||
const frameThickness = 1; // always 1 for now.
|
||||
const outerLineLength =
|
||||
0 +
|
||||
innerLineLength +
|
||||
frameThickness * 2 +
|
||||
options.hPadding * 2 +
|
||||
options.hMargin * 2;
|
||||
|
||||
const outerLineLength = 0
|
||||
+ innerLineLength
|
||||
+ frameThickness * 2
|
||||
+ options.hPadding * 2
|
||||
+ options.hMargin * 2;
|
||||
// get the frame characters from the frameType.
|
||||
let [
|
||||
fNorth, // horizontal frame top lines
|
||||
fSouth, // horizontal frame bottom lines
|
||||
fWest, // vertical frame lines on the left side
|
||||
fEast, // vertical frame lines on the right side
|
||||
fNorthWest, // upper left frame corner
|
||||
fNorthEast, // upper right frame corner
|
||||
fSouthWest, // lower left frame corner
|
||||
fSouthEast, // lower right frame corner
|
||||
] = options.frameChars.split("");
|
||||
if (fNorth === "§") {
|
||||
fNorth = "";
|
||||
}
|
||||
if (fSouth === "§") {
|
||||
fSouth = "";
|
||||
}
|
||||
if (fEast === "§") {
|
||||
fEast = "";
|
||||
}
|
||||
if (fWest === "§") {
|
||||
fWest = "";
|
||||
}
|
||||
if (fNorthEast === "§") {
|
||||
fNorthEast = "";
|
||||
}
|
||||
if (fSouthEast === "§") {
|
||||
fSouthEast = "";
|
||||
}
|
||||
if (fNorthWest === "§") {
|
||||
fNorthWest = "";
|
||||
}
|
||||
if (fSouthWest === "§") {
|
||||
fSouthWest = "";
|
||||
}
|
||||
|
||||
// get the frame characters from the frameType.
|
||||
let [
|
||||
fNorth, // horizontal frame top lines
|
||||
fSouth, // horizontal frame bottom lines
|
||||
fWest, // vertical frame lines on the left side
|
||||
fEast, // vertical frame lines on the right side
|
||||
fNorthWest, // upper left frame corner
|
||||
fNorthEast, // upper right frame corner
|
||||
fSouthWest, // lower left frame corner
|
||||
fSouthEast, // lower right frame corner
|
||||
] = options.frameChars.split("");
|
||||
if (fNorth === "§") { fNorth = ""; }
|
||||
if (fSouth === "§") { fSouth = ""; }
|
||||
if (fEast === "§") { fEast = ""; }
|
||||
if (fWest === "§") { fWest = ""; }
|
||||
if (fNorthEast === "§") { fNorthEast = ""; }
|
||||
if (fSouthEast === "§") { fSouthEast = ""; }
|
||||
if (fNorthWest === "§") { fNorthWest = ""; }
|
||||
if (fSouthWest === "§") { fSouthWest = ""; }
|
||||
let output = "";
|
||||
|
||||
let output = "";
|
||||
//
|
||||
// GENERATE THE MARGIN SPACE ABOVE THE FRAMED TEXT
|
||||
//
|
||||
// ( we insert space characters even though )
|
||||
// ( they wouldn't normally be visible. But )
|
||||
// ( Some fonts might allow us to see blank )
|
||||
// ( space, and what if we want to nest many )
|
||||
// ( frames inside each other? )
|
||||
//
|
||||
output += (options.marginChar.repeat(outerLineLength) + "\n").repeat(
|
||||
options.vMargin,
|
||||
);
|
||||
|
||||
//
|
||||
// GENERATE THE MARGIN SPACE ABOVE THE FRAMED TEXT
|
||||
//
|
||||
// ( we insert space characters even though )
|
||||
// ( they wouldn't normally be visible. But )
|
||||
// ( Some fonts might allow us to see blank )
|
||||
// ( space, and what if we want to nest many )
|
||||
// ( frames inside each other? )
|
||||
//
|
||||
output += (options.marginChar.repeat(outerLineLength) + "\n").repeat(options.vMargin);
|
||||
//
|
||||
// GENERATE THE TOP PART OF THE FRAME
|
||||
// ╔════════════╗
|
||||
//
|
||||
//
|
||||
output +=
|
||||
"" + // Make sure JS knows we're adding a string.
|
||||
options.marginChar.repeat(options.hMargin) + // the margin before the frame starts
|
||||
fNorthWest + // northwest frame corner
|
||||
fNorth.repeat(innerLineLength + options.hPadding * 2) + // the long horizontal frame top bar
|
||||
fNorthEast + // northeast frame corner
|
||||
options.marginChar.repeat(options.hMargin) + // the margin after the frame ends
|
||||
"\n";
|
||||
//
|
||||
// GENERATE UPPER PADDING
|
||||
//
|
||||
// ║ ║
|
||||
//
|
||||
// (the blank lines within the frame and above the text)
|
||||
output += (
|
||||
options.marginChar.repeat(options.hMargin) +
|
||||
fWest +
|
||||
options.paddingChar.repeat(innerLineLength + options.hPadding * 2) +
|
||||
fEast +
|
||||
options.marginChar.repeat(options.hMargin) +
|
||||
"\n"
|
||||
).repeat(options.vPadding);
|
||||
|
||||
//
|
||||
// GENERATE FRAMED TEXT SEGMENT
|
||||
//
|
||||
// ║ My pretty ║
|
||||
// ║ text here ║
|
||||
//
|
||||
// ( this could be done with a reduce() )
|
||||
//
|
||||
for (const line of lines) {
|
||||
output +=
|
||||
"" + // Make sure JS knows we're adding a string.
|
||||
options.marginChar.repeat(options.hMargin) + // margin before frame
|
||||
fWest + // vertical frame char
|
||||
options.paddingChar.repeat(options.hPadding) + // padding before text
|
||||
line.padEnd(innerLineLength, " ") + // The actual text. Pad it with normal space character, NOT custom space.
|
||||
options.paddingChar.repeat(options.hPadding) + // padding after text
|
||||
fEast + // vertical frame bar
|
||||
options.marginChar.repeat(options.hMargin) + // margin after frame
|
||||
"\n";
|
||||
}
|
||||
|
||||
//
|
||||
// GENERATE THE TOP PART OF THE FRAME
|
||||
// ╔════════════╗
|
||||
//
|
||||
//
|
||||
output += "" // Make sure JS knows we're adding a string.
|
||||
+ options.marginChar.repeat(options.hMargin) // the margin before the frame starts
|
||||
+ fNorthWest // northwest frame corner
|
||||
+ fNorth.repeat(innerLineLength + options.hPadding * 2) // the long horizontal frame top bar
|
||||
+ fNorthEast // northeast frame corner
|
||||
+ options.marginChar.repeat(options.hMargin) // the margin after the frame ends
|
||||
+ "\n";
|
||||
//
|
||||
// GENERATE UPPER PADDING
|
||||
//
|
||||
// ║ ║
|
||||
//
|
||||
// (the blank lines within the frame and above the text)
|
||||
output += (
|
||||
options.marginChar.repeat(options.hMargin)
|
||||
+ fWest
|
||||
+ options.paddingChar.repeat(innerLineLength + options.hPadding * 2)
|
||||
+ fEast
|
||||
+ options.marginChar.repeat(options.hMargin)
|
||||
+ "\n"
|
||||
).repeat(options.vPadding);
|
||||
//
|
||||
// GENERATE LOWER PADDING
|
||||
//
|
||||
// ║ ║
|
||||
//
|
||||
// ( the blank lines within the )
|
||||
// ( frame and below the text )
|
||||
//
|
||||
// ( this code is a direct )
|
||||
// ( repeat of the code that )
|
||||
// ( generates top padding )
|
||||
output += (
|
||||
options.marginChar.repeat(options.hMargin) +
|
||||
fWest +
|
||||
options.paddingChar.repeat(innerLineLength + options.hPadding * 2) +
|
||||
fEast +
|
||||
options.marginChar.repeat(options.hMargin) +
|
||||
"\n"
|
||||
).repeat(options.vPadding);
|
||||
|
||||
//
|
||||
// GENERATE FRAMED TEXT SEGMENT
|
||||
//
|
||||
// ║ My pretty ║
|
||||
// ║ text here ║
|
||||
//
|
||||
// ( this could be done with a reduce() )
|
||||
//
|
||||
for (const line of lines) {
|
||||
output += "" // Make sure JS knows we're adding a string.
|
||||
+ options.marginChar.repeat(options.hMargin) // margin before frame
|
||||
+ fWest // vertical frame char
|
||||
+ options.paddingChar.repeat(options.hPadding) // padding before text
|
||||
+ line.padEnd(innerLineLength, " ") // The actual text. Pad it with normal space character, NOT custom space.
|
||||
+ options.paddingChar.repeat(options.hPadding) // padding after text
|
||||
+ fEast // vertical frame bar
|
||||
+ options.marginChar.repeat(options.hMargin) // margin after frame
|
||||
+ "\n";
|
||||
}
|
||||
//
|
||||
// GENERATE THE BOTTOM PART OF THE FRAME
|
||||
//
|
||||
// ╚════════════╝
|
||||
//
|
||||
output +=
|
||||
"" + // Make sure JS knows we're adding a string.
|
||||
options.marginChar.repeat(options.hMargin) + // the margin before the frame starts
|
||||
fSouthWest + // northwest frame corner
|
||||
fSouth.repeat(innerLineLength + options.hPadding * 2) + // the long horizontal frame top bar
|
||||
fSouthEast + // northeast frame corner
|
||||
options.marginChar.repeat(options.hMargin) + // the margin after the frame starts
|
||||
"\n";
|
||||
|
||||
//
|
||||
// GENERATE LOWER PADDING
|
||||
//
|
||||
// ║ ║
|
||||
//
|
||||
// ( the blank lines within the )
|
||||
// ( frame and below the text )
|
||||
//
|
||||
// ( this code is a direct )
|
||||
// ( repeat of the code that )
|
||||
// ( generates top padding )
|
||||
output += (
|
||||
options.marginChar.repeat(options.hMargin)
|
||||
+ fWest
|
||||
+ options.paddingChar.repeat(innerLineLength + options.hPadding * 2)
|
||||
+ fEast
|
||||
+ options.marginChar.repeat(options.hMargin)
|
||||
+ "\n"
|
||||
).repeat(options.vPadding);
|
||||
//
|
||||
// GENERATE THE MARGIN SPACE BELOW THE FRAMED TEXT
|
||||
//
|
||||
// ( we insert space characters even though )
|
||||
// ( they wouldn't normally be visible. But )
|
||||
// ( Some fonts might allow us to see blank )
|
||||
// ( space, and what if we want to nest many )
|
||||
// ( frames inside each other? )
|
||||
//
|
||||
output += (options.marginChar.repeat(outerLineLength) + "\n").repeat(
|
||||
options.vMargin,
|
||||
);
|
||||
|
||||
|
||||
//
|
||||
// GENERATE THE BOTTOM PART OF THE FRAME
|
||||
//
|
||||
// ╚════════════╝
|
||||
//
|
||||
output += "" // Make sure JS knows we're adding a string.
|
||||
+ options.marginChar.repeat(options.hMargin) // the margin before the frame starts
|
||||
+ fSouthWest // northwest frame corner
|
||||
+ fSouth.repeat(innerLineLength + options.hPadding * 2) // the long horizontal frame top bar
|
||||
+ fSouthEast // northeast frame corner
|
||||
+ options.marginChar.repeat(options.hMargin) // the margin after the frame starts
|
||||
+ "\n";
|
||||
|
||||
//
|
||||
// GENERATE THE MARGIN SPACE BELOW THE FRAMED TEXT
|
||||
//
|
||||
// ( we insert space characters even though )
|
||||
// ( they wouldn't normally be visible. But )
|
||||
// ( Some fonts might allow us to see blank )
|
||||
// ( space, and what if we want to nest many )
|
||||
// ( frames inside each other? )
|
||||
//
|
||||
output += (options.marginChar.repeat(outerLineLength) + "\n").repeat(options.vMargin);
|
||||
|
||||
return output;
|
||||
return output;
|
||||
}
|
||||
|
||||
// Allow this script to be run directly from node as well as being included!
|
||||
|
||||
Reference in New Issue
Block a user