cassidy-styler
Version:
A versatile library to style your texts using various Unicode fonts and formatting options.
676 lines (567 loc) • 17.6 kB
text/typescript
export { default as FontSystem } from "./font";
import { fonts, FontTypes } from "./font";
const line = "━";
/**
* Formats a title string by extracting and rearranging emojis and non-emoji characters based on a pattern.
*
* @param {string} str - The input string containing emojis and text.
* @param {string} [pattern] - The format pattern where `{word}` represents non-emoji text and `{emojis}` represents extracted emojis.
* @returns {string} - The formatted title string.
*/
export function forceTitleFormat(str: string, pattern?: string): string {
pattern ??= `{word} ${UNIRedux.charm} {emojis}`;
const emojiRegex = /\p{Emoji}/gu;
let emojis = [...str].filter((char) => emojiRegex.test(char)).join("");
let nonEmojis = [...str]
.filter((char) => !emojiRegex.test(char))
.join("")
.trim()
.replaceAll("|", "");
const res = pattern
.replaceAll("{word}", nonEmojis)
.replaceAll("{emojis}", emojis);
return res;
}
interface FormatOptions {
title: string;
content: string;
titleFont?: FontTypes;
contentFont?: FontTypes;
titlePattern?: string;
noFormat?: boolean;
lineLength?: number;
}
/**
* Formats title and content text.
*/
export function format(
title: string,
content: string,
contentFont?: FontTypes
): string;
/**
* Formats title and content text with optional font styles and title patterns.
*/
export function format({
title,
content,
contentFont,
titleFont,
titlePattern,
noFormat,
lineLength,
}: FormatOptions): string;
/**
* Formats title and content text with optional font styles and title patterns.
*/
export function format(
arg1: string | FormatOptions,
arg2?: string,
arg3?: FontTypes | undefined
): string {
let options: FormatOptions;
if (typeof arg1 === "string" && typeof arg2 === "string") {
options = { title: arg1, content: arg2, contentFont: arg3 };
} else if (typeof arg1 === "object" && arg1 !== null) {
options = arg1;
} else {
throw new Error("Invalid arguments");
}
options.titleFont ??= "bold";
options.contentFont ??= "fancy";
options.titlePattern ??= undefined;
options.noFormat ??= false;
options.lineLength ??= 15;
return `${fonts[options.titleFont](
!options.noFormat
? forceTitleFormat(options.title, options.titlePattern)
: options.title
)}\n${line.repeat(options.lineLength)}\n${fonts[options.contentFont](
autoBold(options.content)
)}`;
}
/**
* A collection of special Unicode characters and symbols.
* Provides commonly used characters like line separators, trademarks, mathematical symbols, and more.
*/
export class UNIRedux {
/** Special invisible space character */
static specialSpace = "ᅠ";
/** Standard line repeated 1 time */
static singleLine = line;
/** Burger menu icon */
static burger: "☰" = "☰";
/** Standard line repeated 15 times */
static standardLine = line.repeat(15);
/** Section sign */
static section: "§" = "§";
/** Pilcrow sign */
static paragraph: "¶" = "¶";
/** Registered trademark sign */
static registered: "®" = "®";
/** Trademark sign */
static trademark: "™" = "™";
/** Copyright sign */
static copyright: "©" = "©";
/** Degree sign */
static degree: "°" = "°";
/** Micro sign */
static micro: "µ" = "µ";
/** Bullet point */
static bullet: "•" = "•";
/** En dash */
static enDash: "–" = "–";
/** Em dash */
static emDash: "—" = "—";
/** Prime symbol */
static prime: "′" = "′";
/** Double prime symbol */
static doublePrime: "″" = "″";
/** Dagger symbol */
static daggers: "†" = "†";
/** Double dagger symbol */
static doubleDagger: "‡" = "‡";
/** Ellipsis */
static ellipsis: "…" = "…";
/** Infinity symbol */
static infinity: "∞" = "∞";
/** Generic currency sign */
static currency: "¤" = "¤";
/** Yen sign */
static yen: "¥" = "¥";
/** Euro sign */
static euro: "€" = "€";
/** Pound sign */
static pound: "£" = "£";
/** Plus-minus sign */
static plusMinus: "±" = "±";
/** Approximately equal sign */
static approximately: "≈" = "≈";
/** Not equal to sign */
static notEqual: "≠" = "≠";
/** Less than or equal to sign */
static lessThanOrEqual: "≤" = "≤";
/** Greater than or equal to sign */
static greaterThanOrEqual: "≥" = "≥";
/** Summation sign */
static summation: "∑" = "∑";
/** Integral sign */
static integral: "∫" = "∫";
/** Square root sign */
static squareRoot: "√" = "√";
/** Partial differential sign */
static partialDifferential: "∂" = "∂";
/** Angle symbol */
static angle: "∠" = "∠";
/** Degree Fahrenheit sign */
static degreeFahrenheit: "℉" = "℉";
/** Degree Celsius sign */
static degreeCelsius: "℃" = "℃";
/** Floral Heart symbol */
static floralHeart: "❧" = "❧";
/** Star Flower symbol */
static starFlower: "✻" = "✻";
/** Heavy Star symbol */
static heavyStar: "★" = "★";
/** Sparkle symbol */
static sparkle: "✦" = "✦";
/** Asterisk symbol */
static asterisk: "✱" = "✱";
/** Heavy Check Mark */
static heavyCheckMark: "✔" = "✔";
/** Heavy Ballot X */
static heavyBallotX: "✖" = "✖";
/** Heart symbol */
static heart: "♥" = "♥";
/** Diamond symbol */
static diamond: "♦" = "♦";
/** Club symbol */
static club: "♣" = "♣";
/** Spade symbol */
static spade: "♠" = "♠";
/** Musical Note symbol */
static musicalNote: "♪" = "♪";
/** Double Musical Note symbol */
static doubleMusicalNote: "♫" = "♫";
/** Snowflake symbol */
static snowflake: "❄" = "❄";
/** Sparkle Star symbol */
static sparkleStar: "✨" = "✨";
/** Anchor symbol */
static anchor: "⚓" = "⚓";
/** Umbrella symbol */
static umbrella: "☔" = "☔";
/** Hourglass symbol */
static hourglass: "⌛" = "⌛";
/** Hourglass Not Done symbol */
static hourglassNotDone: "⏳" = "⏳";
/** Charm symbol */
static charm: "✦" = "✦";
/** Disc symbol */
static disc: "⦿" = "⦿";
/** Arrow symbol */
static arrow: "➤" = "➤";
/** Arrow (Black and White) symbol */
static arrowBW: "➣" = "➣";
/** Arrow from Top symbol */
static arrowFromT: "➥" = "➥";
/** Arrow from Bottom symbol */
static arrowFromB: "➦" = "➦";
/** Restart symbol */
static restart: "⟳" = "⟳";
/** Arrow Outline symbol */
static arrowOutline: "➩" = "➩";
}
/**
* Abbreviates a number using K (thousand), M (million), B (billion), etc.
*
* @param {number|string} value - The number to abbreviate.
* @param {number} [places=2] - The number of decimal places to round to.
* @param {boolean} [isFull=false] - If true, returns the full name instead of letter notation (e.g., "Thousand" instead of "K").
* @returns {string} - The abbreviated number.
*/
export function abbreviateNumber(
value: number | string,
places = 2,
isFull = false
): string {
let num = Number(value);
if (isNaN(num)) return "Invalid input";
if (num < 1000) {
return num.toFixed(places).replace(/\.?0+$/, "");
}
const suffixes = ["", "K", "M", "B", "T", "P", "E"];
const fullSuffixes = [
"",
"Thousand",
"Million",
"Billion",
"Trillion",
"Quadrillion",
"Quintillion",
];
const magnitude = Math.floor(Math.log10(num) / 3);
if (magnitude === 0) {
return num % 1 === 0
? num.toString()
: num.toFixed(places).replace(/\.?0+$/, "");
}
const abbreviatedValue = num / Math.pow(1000, magnitude);
const suffix = isFull ? fullSuffixes[magnitude] : suffixes[magnitude];
if (abbreviatedValue % 1 === 0) {
return `${Math.round(abbreviatedValue)}${isFull ? ` ${suffix}` : suffix}`;
}
const formattedValue = abbreviatedValue.toFixed(places).replace(/\.?0+$/, "");
return `${formattedValue}${isFull ? ` ${suffix}` : suffix}`;
}
/**
* Transforms the input text by applying bold and bold-italic formatting.
*
* The function looks for text wrapped in `***` and `**` and replaces them with
* bold-italic and bold formatting respectively.
*
* @param text - The input text to be transformed.
* @returns The transformed text with bold and bold-italic formatting applied.
*/
export function autoBold(text: string) {
text = String(text);
text = text.replace(/\*\*\*(.*?)\*\*\*/g, (_: string, text: string) =>
fonts.bold_italic(text)
);
text = text.replace(/\*\*(.*?)\*\*/g, (_: string, text: string) =>
fonts.bold(text)
);
return text;
}
/**
* Replaces custom font tags in the given text with corresponding font styles.
*
* The function looks for patterns in the format `[font=fontName]text[:font=fontName]`
* and replaces them with the corresponding font styles if the font names match.
*
* @param text - The input text containing custom font tags.
* @returns The text with font tags replaced by corresponding font styles.
*/
export function fontTag(text: string) {
text = String(text);
text = text.replace(
/\[font=(.*?)\]\s*(.*?)\s*\[:font=(.*?)\]/g,
(_, font, text, font2) =>
font === font2 ? fonts[font as FontTypes](text) : text
);
return text;
}
type StrictMessageForm = {
body?: string;
attachment?: ReadableStream | ReadableStream[] | any | any[];
mentions?: Mention[];
location?: { latitude: number; longitude: number; current: boolean };
};
type MessageForm = string | StrictMessageForm;
type FCAID = string | number;
type Mention = {
tag: string;
id: FCAID;
fromIndex: number;
};
interface LiaIOQueue {
form: MessageForm;
senderID?: FCAID;
replyTo?: FCAID | undefined;
style?: FormatOptions;
resolve?: (value: any) => any;
reject?: (reason?: any) => any;
event?: any;
api?: any;
}
/**
* @lianecagara
* Class representing the LiaIOLite/Box for handling message input/output operations.
* This class is responsible for sending, replying, and receiving messages,
* as well as managing message reactions and handling events related to messages.
*
* @class Box
*/
export class Box {
#api: any = null;
#event: any = null;
public style: FormatOptions | undefined;
/**
* Creates an instance of the LiaIO class to manage message interactions.
*
* @param {API} api - The API instance for interacting with the messaging service.
* @param {FCAMessageReplyEvent | any} event - The event that triggered the interaction.
* @memberof Box
*/
constructor(api: any, event: any, style?: FormatOptions) {
this.#api = api;
this.#event = event;
this.style = style;
}
static queue: LiaIOQueue[] = [];
/**
* Sends an output message, which can be a reply or a new message.
*
* @param params - The parameters for sending the message.
* @param params.form - The form of the message to be sent.
* @param params.senderID - The ID of the sender (optional).
* @param params.replyTo - The ID of the message being replied to (optional).
* @param style
* @returns A promise resolving to the sent message event.
* @memberof Box
*/
async out(param0: {
form: MessageForm;
senderID?: FCAID;
replyTo?: FCAID;
style?: FormatOptions;
}): Promise<any> {
const {
form: oform,
senderID = this.#event.threadID,
replyTo = undefined,
style = null,
} = param0;
const form = normalizeMessageForm(oform) as StrictMessageForm;
let exMents: Mention[] = [];
if (typeof form.body === "string") {
const ments = form.body.match(/@\[(.*?)=(.*?)\]/g);
if (Array.isArray(ments)) {
for (const ment of ments) {
const [tag, uid] = ment.slice(2, -1).split("=");
form.body = form.body.replace(ment, `@${tag}`);
exMents.push({
id: uid,
tag,
fromIndex: form.body.indexOf(`@${tag}`),
});
}
}
}
let styler: FormatOptions | undefined = this.style;
if (style) {
styler = style;
}
if (styler && form.body && styler !== undefined && styler.title) {
const combined: FormatOptions = {
...styler,
content: form.body,
};
form.body = format(combined);
}
return new Promise(async (resolve, reject) => {
form.mentions = [...exMents, ...(form.mentions ?? [])];
for (const key in form) {
if (
form[key as keyof StrictMessageForm] === null ||
form[key as keyof StrictMessageForm] === undefined
) {
delete form[key as keyof StrictMessageForm];
}
if (!form.mentions || form.mentions.length < 1) {
delete form.mentions;
}
}
console.log(`Form to send:`, form, senderID, replyTo);
/**
* @type {LiaIOQueue}
*/
const queueItem: LiaIOQueue = {
...param0,
senderID,
replyTo,
style: styler,
form,
resolve,
reject,
api: this.#api,
event: this.#event,
};
Box.queue.push(queueItem);
if (Box.queue.length === 1) {
Box._processQueue();
}
});
}
static async _processQueue() {
console.log(`Processing Queue..`);
while (this.queue.length > 0) {
const currentTask = this.queue[0];
console.log(
`Current Queue task (total ${this.queue.length}):`,
currentTask.form
);
if (this.queue.length > 1) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
try {
console.log(`Sending form...`, currentTask.form);
const {
api,
form: oform,
reject,
resolve,
replyTo,
senderID,
} = currentTask;
const form = normalizeMessageForm(oform);
api.sendMessage(
form,
senderID,
(err: any, info: any) => {
if (err && reject) {
reject(err);
} else if (resolve) {
console.log(`Form sent:`, form, senderID, replyTo);
resolve(info);
}
},
replyTo ?? undefined
);
} catch (error) {
currentTask.reject?.(error);
}
this.queue.shift();
console.log(`Moving to next queue`);
}
}
/**
* Sends a reply to a message, optionally targeting a specific reply.
*
* @param form - The form of the reply message to be sent.
* @param replyTo - The ID of the message being replied to (optional).
* @returns A promise resolving to the message reply event.
* @memberof Box
* @example
* await liaIO.reply("Hello, world!");
*/
reply(
form: MessageForm,
replyTo: FCAID = this.#event.messageID
): Promise<any> {
return this.out({
form,
replyTo,
});
}
/**
* Sends a message to a destination, optionally specifying the destination ID.
*
* @param form - The form of the message to be sent.
* @param senderID - The ID of the destination to send the message to (optional).
* @memberof Box
* @example
* await liaIO.send("Hello, world!");
*/
send(
form: MessageForm,
senderID: FCAID = this.#event.threadID
): Promise<any> {
return this.out({
form,
senderID,
});
}
/**
* An easy way to handle errors.
*
* @param error - Error to be sent.
*/
error(error: Error | Record<string, any>): Promise<any> {
const errString =
error instanceof Error
? String(error.stack)
: JSON.stringify(error, null, 2);
console.error(error);
return this.reply(errString);
}
/**
* Adds a reaction to a message, optionally targeting a specific message to react to.
*
* @param emoji - The reaction to be added (e.g., "like", "love").
* @param reactTo - The ID of the message to react to (optional).
* @returns A promise resolving to the sent reaction event.
* @memberof Box
*/
reaction(
emoji: string,
reactTo: FCAID = this.#event.messageID
): Promise<any> {
return new Promise((resolve, reject) => {
this.#api.setMessageReaction(emoji, reactTo, (err: any) => {
if (err) {
return reject(err);
}
return resolve(true);
});
});
}
clone(): Box {
return new Box(this.#api, this.#event, this.style);
}
styled(style: FormatOptions) {
return new Box(this.#api, this.#event, style);
}
}
function normalizeMessageForm(form: MessageForm): StrictMessageForm {
let r: Record<string, any> = {};
if (form && r) {
if (typeof form === "object") {
r = form;
}
if (typeof form === "string") {
r = {
body: form,
};
}
if (!Array.isArray(r.attachment) && r.attachment) {
r.attachment = [r.attachment];
}
return r;
} else {
return {
body: undefined,
};
}
}
export const LiaIOLite = Box;