irclib/src/utility/message-formatting.ts

226 lines
5.2 KiB
TypeScript

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',
/**
* <ins>underline text</ins>, `^_`
*/
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<string, string>)[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) =>
`<format${fg ? ` color="${fg}"` : ''}${bg ? ` background="${bg}"` : ''}${
fmt ? ` text="${fmt}"` : ''
}>${text}</format>`;
/**
* Wrap regions of text that have formatting or colors with a custom wrapper, e.g. HTML.
* Defaults to a psuedo-XML style wrapper `<format color background text />`
* @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, '');
}