392 lines
9.4 KiB
TypeScript
392 lines
9.4 KiB
TypeScript
/* Recommended usage
|
|
16-bit colors:
|
|
black
|
|
darkblue
|
|
green
|
|
red
|
|
brown
|
|
purple
|
|
gold
|
|
yellow
|
|
limegreen
|
|
cyan
|
|
lightblue
|
|
blue
|
|
pink
|
|
darkgray
|
|
gray
|
|
white
|
|
or any hex value prepended with #
|
|
|
|
Formats:
|
|
action
|
|
code
|
|
multicode (multiline code)
|
|
bold
|
|
italics
|
|
emphasis
|
|
strike
|
|
underline
|
|
*/
|
|
|
|
import { thousandsSeparator, timeSince, toHHMMSS } from '../common';
|
|
import { ProtocolFeatureFlag } from './protocol-flags';
|
|
|
|
// Color Utilities
|
|
|
|
export function colorDistance(s: string, t: string): number {
|
|
if (!s.length || !t.length) {
|
|
return 0;
|
|
}
|
|
|
|
return colorDistance(s.slice(2), t.slice(2)) +
|
|
Math.abs(parseInt(s.slice(0, 2), 16) - parseInt(t.slice(0, 2), 16));
|
|
}
|
|
|
|
export function approximateB16Color(cl: string): string {
|
|
const arr = [];
|
|
let closest = 'white';
|
|
|
|
for (const i in b16Colors) {
|
|
arr.push(b16Colors[i].substring(1));
|
|
}
|
|
|
|
cl = cl.substring(1);
|
|
|
|
arr.sort((a, b) => {
|
|
return colorDistance(a, cl) - colorDistance(b, cl);
|
|
});
|
|
|
|
for (const i in b16Colors) {
|
|
if (b16Colors[i] === '#' + arr[0]) {
|
|
closest = i;
|
|
}
|
|
}
|
|
|
|
return closest;
|
|
}
|
|
|
|
export function b16toHex(name: string): string {
|
|
if (!b16Colors[name]) {
|
|
return '#000000';
|
|
}
|
|
|
|
return b16Colors[name];
|
|
}
|
|
|
|
export interface IMethod {
|
|
start: string;
|
|
end: string;
|
|
}
|
|
|
|
export declare type Keyed = {[key: string]: string};
|
|
export declare type Method = {[key: string]: IMethod};
|
|
|
|
/**
|
|
* Color-hex relations
|
|
*/
|
|
export const b16Colors: Keyed = {
|
|
black: '#000000',
|
|
darkblue: '#00007f',
|
|
green: '#009300',
|
|
red: '#ff0000',
|
|
brown: '#7f0000',
|
|
purple: '#9c009c',
|
|
gold: '#fc7f00',
|
|
yellow: '#ffff00',
|
|
limegreen: '#00fc00',
|
|
cyan: '#00ffff',
|
|
lightblue: '#0000fc',
|
|
blue: '#009393',
|
|
pink: '#ff00ff',
|
|
darkgray: '#7f7f7f',
|
|
gray: '#d2d2d2',
|
|
white: '#ffffff'
|
|
};
|
|
|
|
/**
|
|
* Base class for message formatting
|
|
*/
|
|
export class Formatter {
|
|
/**
|
|
* Available colors.
|
|
*/
|
|
public colors: Keyed = {};
|
|
|
|
/**
|
|
* Available formatting methods.
|
|
*/
|
|
public formatting: Method = {};
|
|
|
|
/**
|
|
* Color escape character, if available.
|
|
*/
|
|
public colorEscape = '';
|
|
|
|
/** Replacements for string escape function */
|
|
public escapeReplacements: [RegExp, string, string][] = [];
|
|
|
|
constructor(
|
|
/**
|
|
* @deprecated Use feature flags
|
|
*/
|
|
public supportFormatting = false,
|
|
/**
|
|
* @deprecated Use feature flags
|
|
*/
|
|
public supportColors = false,
|
|
public flags: ProtocolFeatureFlag[] = []
|
|
) {}
|
|
|
|
/**
|
|
* Wrap a string with code which renders colors.
|
|
* @param color Color
|
|
* @param msg Message
|
|
* @returns Wrapped string
|
|
*/
|
|
public color(color: string, msg: string): string {
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Wrap a string with code which renders message formatting.
|
|
* @param method Format to use
|
|
* @param msg Message
|
|
* @returns Wrapped string
|
|
*/
|
|
public format(method: string, msg: string): string {
|
|
if (!this.supports(ProtocolFeatureFlag.FORMATTING) && !this.supportFormatting) {
|
|
return msg;
|
|
}
|
|
|
|
if (!this.formatting[method]) {
|
|
return msg;
|
|
}
|
|
|
|
if (this.formatting[method].start && this.formatting[method].end) {
|
|
return this.formatting[method].start + msg + this.formatting[method].end;
|
|
} else {
|
|
return this.formatting[method] + msg + this.formatting[method];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Strip any extraneous tags/info.
|
|
* @param msg Message
|
|
* @returns Stripped message
|
|
*/
|
|
public strip(msg: string): string {
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Escape any extraneous tags/info.
|
|
* @param msg Message
|
|
* @returns Escaped message
|
|
*/
|
|
public escape(msg: string): string {
|
|
return this.escapeReplacements.reduce(
|
|
(str, replacement) => str.replace(replacement[0], replacement[1]),
|
|
msg
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Object compositor.
|
|
*
|
|
* This default function turns objects into plain text without any formatting.
|
|
* Override this in your protocol for the desired effect.
|
|
*
|
|
* ```js
|
|
* [
|
|
* ['element type', 'text', { param: value }],
|
|
* ...
|
|
* ]
|
|
* ```
|
|
*
|
|
* Element types:
|
|
* * field - A field type
|
|
* * Parameters:
|
|
* * label - Label for this field.
|
|
* If an array, the first item is considered an "icon" and wont have ':' appended to it.
|
|
* * type - The type of the field.
|
|
* * title - A title field
|
|
* * description - Descriptive field
|
|
* * metric - A Number value. Requires integer, will be transformed into xxx,xxx,xxx
|
|
* * time - Time value. Requires UNIX timestamp.
|
|
* * timesince - Time since value. Requires UNIX timestamp, will be transformed into x seconds/minutes/hours/days ago
|
|
* * duration - Duration value. Requires integer, will be transformed into HH:MM:SS
|
|
* * content - Full message body.
|
|
* * color - The color of the field. Not always supported.
|
|
* * bold/b/strong - Bold text
|
|
* * i/italic - Italic text
|
|
* * color - A colored text. Not always supported.
|
|
* * url - An URL.
|
|
* * Parameters:
|
|
* * label - Label for this URL
|
|
* * image - An Image.
|
|
*/
|
|
public compose(objs: any): any {
|
|
const str = [];
|
|
|
|
for (const i in objs) {
|
|
const elem = objs[i];
|
|
|
|
const elemType = elem[0];
|
|
let elemValue = elem[1];
|
|
const elemParams = elem[2];
|
|
|
|
if (!elemValue) {
|
|
continue;
|
|
}
|
|
|
|
// Special types
|
|
if (elemParams && elemParams.type) {
|
|
switch (elemParams.type) {
|
|
case 'time':
|
|
elemValue = new Date(elemValue).toString();
|
|
break;
|
|
case 'metric':
|
|
elemValue = thousandsSeparator(elemValue);
|
|
break;
|
|
case 'timesince':
|
|
elemValue = timeSince(elemValue);
|
|
break;
|
|
case 'duration':
|
|
elemValue = toHHMMSS(elemValue);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (elemParams && elemParams.label) {
|
|
let label = elemParams.label;
|
|
|
|
// If the label param is an array, choose the last element
|
|
// The last element is generally the text version, as opposed to
|
|
// the first element being an icon.
|
|
if (typeof label === 'object') {
|
|
label = elemParams.label[elemParams.label.length - 1];
|
|
}
|
|
|
|
str.push(label + ': ' + elemValue);
|
|
} else {
|
|
str.push(elemValue);
|
|
}
|
|
}
|
|
|
|
// May return an object, but your protocol must support it.
|
|
return str.join(' ');
|
|
}
|
|
|
|
/**
|
|
* Check if this protocol supports a feature flag
|
|
* @param flag Feature flag to check for
|
|
* @returns Boolean
|
|
*/
|
|
public supports(flag: ProtocolFeatureFlag | ProtocolFeatureFlag[]) {
|
|
return Array.isArray(flag)
|
|
? flag.every((entry) => this.flags.includes(entry))
|
|
: this.flags.includes(flag);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Built-in formatter for HTML-enabled protocols
|
|
*/
|
|
export class HTMLFormatter extends Formatter {
|
|
public formatting: Method = {
|
|
code: {start: '<code>', end: '</code>'},
|
|
action: {start: '<i>', end: '</i>'},
|
|
bold: {start: '<b>', end: '</b>'},
|
|
italics: {start: '<i>', end: '</i>'},
|
|
emphasis: {start: '<em>', end: '</em>'},
|
|
underline: {start: '<u>', end: '</u>'},
|
|
multicode: {start: '<pre><code>', end: '</code></pre>'},
|
|
};
|
|
|
|
public escapeReplacements: [RegExp, string, string][] = [
|
|
[/&/g, '&', 'ampersand'],
|
|
[/</g, '<', 'angle brackets'],
|
|
[/>/g, '>', 'angle brackets'],
|
|
[/"/g, '"', 'quotes'],
|
|
[/'/g, ''', 'single quates'],
|
|
];
|
|
|
|
constructor(flags?: ProtocolFeatureFlag[]) {
|
|
super(true, true, flags);
|
|
}
|
|
|
|
public color(color: string, msg: string): string {
|
|
return `<span style="color:${color};">${msg}</span>`;
|
|
}
|
|
|
|
public format(method: string, msg: string): string {
|
|
if (!this.formatting[method]) {
|
|
return msg;
|
|
}
|
|
|
|
if (this.formatting[method].start && this.formatting[method].end) {
|
|
return this.formatting[method].start + msg + this.formatting[method].end;
|
|
} else {
|
|
return this.formatting[method] + msg + this.formatting[method];
|
|
}
|
|
}
|
|
|
|
public strip(msg: string): string {
|
|
return msg.replace(/<\/?[^>]+(>|$)/g, '');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Built-in formatter for Markdown-enabled protocols
|
|
*/
|
|
export class MarkdownFormatter extends Formatter {
|
|
public formatting: Method = {
|
|
code: {start: '`', end: '`'},
|
|
bold: {start: '**', end: '**'},
|
|
action: {start: '*', end: '*'},
|
|
italics: {start: '*', end: '*'},
|
|
emphasis: {start: '*', end: '*'},
|
|
underline: {start: '__', end: '__'},
|
|
multicode: {start: '```', end: '```'},
|
|
};
|
|
|
|
public escapeReplacements: [RegExp, string, string][] = [
|
|
[/\*/g, '\\*', 'asterisks'],
|
|
[/#/g, '\\#', 'number signs'],
|
|
[/\//g, '\\/', 'slashes'],
|
|
[/\(/g, '\\(', 'parentheses'],
|
|
[/\)/g, '\\)', 'parentheses'],
|
|
[/\[/g, '\\[', 'square brackets'],
|
|
[/\]/g, '\\]', 'square brackets'],
|
|
[/</g, '<', 'angle brackets'],
|
|
[/>/g, '>', 'angle brackets'],
|
|
[/_/g, '\\_', 'underscores'],
|
|
[/`/g, '\\`', 'codeblocks']
|
|
];
|
|
|
|
constructor(flags?: ProtocolFeatureFlag[]) {
|
|
super(true, false, flags);
|
|
}
|
|
|
|
public color(color: string, msg: string): string {
|
|
return msg;
|
|
}
|
|
|
|
public format(method: string, msg: string): string {
|
|
if (!this.formatting[method]) {
|
|
return msg;
|
|
}
|
|
|
|
if (this.formatting[method].start && this.formatting[method].end) {
|
|
return this.formatting[method].start + msg + this.formatting[method].end;
|
|
} else {
|
|
return this.formatting[method] + msg + this.formatting[method];
|
|
}
|
|
}
|
|
|
|
public strip(msg: string): string {
|
|
return msg;
|
|
}
|
|
}
|