From d11a983319a0cabc580c0714623325523f9e1ca2 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sat, 24 Sep 2022 12:16:07 +0300 Subject: [PATCH] cool color and format utilities --- src/examples/connection-test.ts | 11 +- src/index.ts | 1 + src/types/events.ts | 2 +- src/utility/message-formatting.ts | 216 ++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/utility/message-formatting.ts diff --git a/src/examples/connection-test.ts b/src/examples/connection-test.ts index 7603263..6c51e2d 100644 --- a/src/examples/connection-test.ts +++ b/src/examples/connection-test.ts @@ -1,4 +1,8 @@ import { IRCBot } from '../bot'; +import { + applyTextColor, + wrapFormattedText, +} from '../utility/message-formatting'; import { NickServValidator } from '../utility/nickserv-validator'; const bot = new IRCBot({ @@ -29,7 +33,7 @@ bot.on('supported-modes', (supported) => { // bot.on('line', console.log); bot.on('message', ({ message, to, nickname }) => { - console.log(`[${to}] ${nickname}: ${message}`); + console.log(`[${to}] ${nickname}: ${wrapFormattedText(message)}`); if (message.startsWith('!test')) { nickserv @@ -46,6 +50,11 @@ bot.on('message', ({ message, to, nickname }) => { return; } + if (message.startsWith('!colors')) { + bot.send(to, `${applyTextColor('blue', 'cool blue text!')}`); + return; + } + if (message.startsWith('!whois')) { bot.whois(nickname).then(console.log); return; diff --git a/src/index.ts b/src/index.ts index 8b276f8..1b64183 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export * from './types/impl.interface'; export * from './utility/collector'; export * from './utility/estimate-prefix'; export * from './utility/formatstr'; +export * from './utility/message-formatting'; export * from './utility/mode-from-prefix'; export * from './utility/nickserv-validator'; export * from './utility/parser'; diff --git a/src/types/events.ts b/src/types/events.ts index aa78d2f..062d4bc 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -14,7 +14,7 @@ export type IRCCommunicatorEvents = { /** * Supported channel user modes from the server (e.g. `ohv: @%+`) */ - 'supported-modes': (modes: Record) => void; + 'supported-modes': (modes: Record) => void; /** * Everything this server supports. See IRC documentation for command `005` or `RPL_ISUPPORT` for more info. */ diff --git a/src/utility/message-formatting.ts b/src/utility/message-formatting.ts new file mode 100644 index 0000000..b2f5d08 --- /dev/null +++ b/src/utility/message-formatting.ts @@ -0,0 +1,216 @@ +export const IRC_FMT_COLOR_MAP = { + /** + * IRC white `rgb(255,255,255)` + */ + white: '\u00030', + /** + * IRC black `rgb(0,0,0)` + */ + black: '\u00031', + /** + * IRC blue `rgb(0,0,127)` + */ + blue: '\u00032', + /** + * IRC green `rgb(0,147,0)` + */ + green: '\u00033', + /** + * IRC red `rgb(255,0,0)` + */ + red: '\u00034', + /** + * IRC brown `rgb(127,0,0)` + */ + brown: '\u00035', + /** + * IRC purple `rgb(156,0,156)` + */ + purple: '\u00036', + /** + * IRC orange `rgb(252,127,0)` + */ + orange: '\u00037', + /** + * IRC yellow `rgb(255,255,0)` + */ + yellow: '\u00038', + /** + * IRC light green `rgb(0,252,0)` + */ + lightgreen: '\u00039', + /** + * IRC cyan `rgb(0,147,147)` + */ + cyan: '\u000310', + /** + * IRC light cyan `rgb(0,255,255)` + */ + lightcyan: '\u000311', + /** + * IRC light blue `rgb(0,0,252)` + */ + lightblue: '\u000312', + /** + * IRC pink `rgb(255,0,255)` + */ + pink: '\u000313', + /** + * IRC grey `rgb(127,127,127)` + */ + grey: '\u000314', + /** + * IRC light grey `rgb(210,210,210)` + */ + lightgrey: '\u000315', +}; + +/** + * ^O character, color reset character + */ +export const IRC_FMT_RESET = '\u000F'; + +export const IRC_FMT_FORMAT_MAP = { + /** + * Normal text + */ + normal: '\u0000', + /** + * underline text, `^_` + */ + underline: '\u001F', + /** + * **bold text**, `^B` + */ + bold: '\u0002', + /** + * *italic text*, `^I` + */ + italics: '\u001D', + /** + * reverse color text, `^V` + */ + reverse: '\u0016', +}; + +/** + * Remove color and format characters from a string. + * @param str Text + * @returns Stripped text + */ +export const stripFormatting = (str: string): string => + str.replace(/(\x03\d{0,2}(,\d{0,2})?)/g, '').replace(/[\x00-\x1F]/g, ''); + +/** + * Colorize a string for sending. + * @param color IRC color key + * @param str Text to colorize + * @returns Text with control characters + */ +export const applyTextColor = ( + color: keyof typeof IRC_FMT_COLOR_MAP, + str: string, +): string => + !IRC_FMT_COLOR_MAP[color] + ? str + : `${IRC_FMT_COLOR_MAP[color]}${str}${IRC_FMT_RESET}`; + +/** + * Apply text formatting to a string for sending. + * @param format IRC format key + * @param str Text to format + * @returns Text with control characters + */ +export const applyTextFormat = ( + format: keyof typeof IRC_FMT_FORMAT_MAP, + str: string, +): string => + !IRC_FMT_FORMAT_MAP[format] + ? str + : `${IRC_FMT_FORMAT_MAP[format]}${str}${IRC_FMT_RESET}`; + +const styleCheckRe = /[\x00-\x1F]/; +const backRe = /^(\d{1,2})(,(\d{1,2}))?/; +const colourKey = '\x03'; +const colourRe = /\x03/g; +const colorKeys = Object.keys(IRC_FMT_COLOR_MAP); +const fmtRegex: [string, RegExp][] = Object.keys(IRC_FMT_FORMAT_MAP).map( + (key) => { + const escaped = encodeURI( + (IRC_FMT_FORMAT_MAP as Record)[key], + ).replace('%', '\\x'); + return [key, new RegExp(escaped + '(.*?)(' + escaped + '|$)')]; + }, +); + +export type IRCTextFormatWrapperFn = ( + text: string, + foregroundColor?: string, + backgroundColor?: string, + textFormat?: string, +) => string; + +const wrap: IRCTextFormatWrapperFn = (text, fg, bg, fmt) => + `${text}`; + +/** + * Wrap regions of text that have formatting or colors with a custom wrapper, e.g. HTML. + * Defaults to a psuedo-XML style wrapper `` + * @param line Text with control characters + * @param wrapperFn Wrapper function to use + * @returns Text with applied wrappers + */ +export function wrapFormattedText(line: string, wrapperFn = wrap): string { + // Recheck + if (!styleCheckRe.test(line)) return line; + + // split up by the irc style break character ^O + if (line.indexOf(IRC_FMT_RESET) >= 0) { + return line + .split(IRC_FMT_RESET) + .map((value) => wrapFormattedText(value, wrapperFn)) + .join(''); + } + + let result = line; + const parseArr = result.split(colourKey); + + for (let i = 0; i < parseArr.length; i++) { + const text = parseArr[i]; + const match = text.match(backRe); + const colour = match && colorKeys[+match[1]]; + let background = ''; + + if (!match || !colour) { + // ^C (no colour) ending. Escape current colour and carry on + background = ''; + continue; + } + + // set the background colour + // we don't override the background local var to support nesting + if (colorKeys[+match[3]]) { + background = colorKeys[+match[3]]; + } + + // update the parsed text result + result = result.replace( + colourKey + text, + wrapperFn(text.slice(match[0].length), colour, background), + ); + } + + // Matching styles (italics/bold/underline) + // if only colors were this easy... + fmtRegex.forEach(function ([style, keyregex]) { + if (result.indexOf(style) < 0) return; + result = result.replace(keyregex, (match, text) => + wrapperFn(text, undefined, undefined, style), + ); + }); + + // replace the reminent colour terminations and be done with it + return result.replace(colourRe, ''); +}