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}`; /** * Apply CTCP control characters with a command. * @param message Message * @param command CTCP command, e.g. `ACTION` * @returns Text with control characters */ export const applyCTCP = (message: string, command = 'ACTION') => `\x01${command.toUpperCase()} ${message}\x01`; 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, ''); }