dd05ee3a65
Co-authored-by: Eric Nemchik <eric@nemchik.com> Co-authored-by: Pavel Djundik <xPaw@users.noreply.github.com>
251 lines
6.1 KiB
TypeScript
251 lines
6.1 KiB
TypeScript
// Styling control codes
|
|
const BOLD = "\x02";
|
|
const COLOR = "\x03";
|
|
const HEX_COLOR = "\x04";
|
|
const RESET = "\x0f";
|
|
const REVERSE = "\x16";
|
|
const ITALIC = "\x1d";
|
|
const UNDERLINE = "\x1f";
|
|
const STRIKETHROUGH = "\x1e";
|
|
const MONOSPACE = "\x11";
|
|
|
|
export type ParsedStyle = {
|
|
bold?: boolean;
|
|
textColor?: string;
|
|
bgColor?: string;
|
|
hexColor?: string;
|
|
hexBgColor?: string;
|
|
italic?: boolean;
|
|
underline?: boolean;
|
|
strikethrough?: boolean;
|
|
monospace?: boolean;
|
|
text: string;
|
|
start: number;
|
|
end: number;
|
|
};
|
|
|
|
// Color code matcher, with format `XX,YY` where both `XX` and `YY` are
|
|
// integers, `XX` is the text color and `YY` is an optional background color.
|
|
const colorRx = /^(\d{1,2})(?:,(\d{1,2}))?/;
|
|
|
|
// 6-char Hex color code matcher
|
|
const hexColorRx = /^([0-9a-f]{6})(?:,([0-9a-f]{6}))?/i;
|
|
|
|
// Represents all other control codes that to be ignored/filtered from the text
|
|
// This regex allows line feed character
|
|
const controlCodesRx = /[\u0000-\u0009\u000B-\u001F]/g;
|
|
|
|
// Converts a given text into an array of objects, each of them representing a
|
|
// similarly styled section of the text. Each object carries the `text`, style
|
|
// information (`bold`, `textColor`, `bgcolor`, `italic`,
|
|
// `underline`, `strikethrough`, `monospace`), and `start`/`end` cursors.
|
|
function parseStyle(text: string) {
|
|
const result: ParsedStyle[] = [];
|
|
let start = 0;
|
|
let position = 0;
|
|
|
|
// At any given time, these carry style information since last time a styling
|
|
// control code was met.
|
|
let colorCodes,
|
|
bold,
|
|
textColor,
|
|
bgColor,
|
|
hexColor,
|
|
hexBgColor,
|
|
italic,
|
|
underline,
|
|
strikethrough,
|
|
monospace;
|
|
|
|
const resetStyle = () => {
|
|
bold = false;
|
|
textColor = undefined;
|
|
bgColor = undefined;
|
|
hexColor = undefined;
|
|
hexBgColor = undefined;
|
|
italic = false;
|
|
underline = false;
|
|
strikethrough = false;
|
|
monospace = false;
|
|
};
|
|
|
|
resetStyle();
|
|
|
|
// When called, this "closes" the current fragment by adding an entry to the
|
|
// `result` array using the styling information set last time a control code
|
|
// was met.
|
|
const emitFragment = () => {
|
|
// Uses the text fragment starting from the last control code position up to
|
|
// the current position
|
|
const textPart = text.slice(start, position);
|
|
|
|
// Filters out all non-style related control codes present in this text
|
|
const processedText = textPart.replace(controlCodesRx, " ");
|
|
|
|
if (processedText.length) {
|
|
// Current fragment starts where the previous one ends, or at 0 if none
|
|
const fragmentStart = result.length ? result[result.length - 1].end : 0;
|
|
|
|
result.push({
|
|
bold,
|
|
textColor,
|
|
bgColor,
|
|
hexColor,
|
|
hexBgColor,
|
|
italic,
|
|
underline,
|
|
strikethrough,
|
|
monospace,
|
|
text: processedText,
|
|
start: fragmentStart,
|
|
end: fragmentStart + processedText.length,
|
|
});
|
|
}
|
|
|
|
// Now that a fragment has been "closed", the next one will start after that
|
|
start = position + 1;
|
|
};
|
|
|
|
// This loop goes through each character of the given text one by one by
|
|
// bumping the `position` cursor. Every time a new special "styling" character
|
|
// is met, an object gets created (with `emitFragment()`)information on text
|
|
// encountered since the previous styling character.
|
|
while (position < text.length) {
|
|
switch (text[position]) {
|
|
case RESET:
|
|
emitFragment();
|
|
resetStyle();
|
|
break;
|
|
|
|
// Meeting a BOLD character means that the ongoing text is either going to
|
|
// be in bold or that the previous one was in bold and the following one
|
|
// must be reset.
|
|
// This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE.
|
|
case BOLD:
|
|
emitFragment();
|
|
bold = !bold;
|
|
break;
|
|
|
|
case COLOR:
|
|
emitFragment();
|
|
|
|
// Go one step further to find the corresponding color
|
|
colorCodes = text.slice(position + 1).match(colorRx);
|
|
|
|
if (colorCodes) {
|
|
textColor = Number(colorCodes[1]);
|
|
|
|
if (colorCodes[2]) {
|
|
bgColor = Number(colorCodes[2]);
|
|
}
|
|
|
|
// Color code length is > 1, so bump the current position cursor by as
|
|
// much (and reset the start cursor for the current text block as well)
|
|
position += colorCodes[0].length;
|
|
start = position + 1;
|
|
} else {
|
|
// If no color codes were found, toggles back to no colors (like BOLD).
|
|
textColor = undefined;
|
|
bgColor = undefined;
|
|
}
|
|
|
|
break;
|
|
|
|
case HEX_COLOR:
|
|
emitFragment();
|
|
|
|
colorCodes = text.slice(position + 1).match(hexColorRx);
|
|
|
|
if (colorCodes) {
|
|
hexColor = colorCodes[1].toUpperCase();
|
|
|
|
if (colorCodes[2]) {
|
|
hexBgColor = colorCodes[2].toUpperCase();
|
|
}
|
|
|
|
// Color code length is > 1, so bump the current position cursor by as
|
|
// much (and reset the start cursor for the current text block as well)
|
|
position += colorCodes[0].length;
|
|
start = position + 1;
|
|
} else {
|
|
// If no color codes were found, toggles back to no colors (like BOLD).
|
|
hexColor = undefined;
|
|
hexBgColor = undefined;
|
|
}
|
|
|
|
break;
|
|
|
|
case REVERSE: {
|
|
emitFragment();
|
|
const tmp = bgColor;
|
|
bgColor = textColor;
|
|
textColor = tmp;
|
|
break;
|
|
}
|
|
|
|
case ITALIC:
|
|
emitFragment();
|
|
italic = !italic;
|
|
break;
|
|
|
|
case UNDERLINE:
|
|
emitFragment();
|
|
underline = !underline;
|
|
break;
|
|
|
|
case STRIKETHROUGH:
|
|
emitFragment();
|
|
strikethrough = !strikethrough;
|
|
break;
|
|
|
|
case MONOSPACE:
|
|
emitFragment();
|
|
monospace = !monospace;
|
|
break;
|
|
}
|
|
|
|
// Evaluate the next character at the next iteration
|
|
position += 1;
|
|
}
|
|
|
|
// The entire text has been parsed, so we finalize the current text fragment.
|
|
emitFragment();
|
|
|
|
return result;
|
|
}
|
|
|
|
const properties = [
|
|
"bold",
|
|
"textColor",
|
|
"bgColor",
|
|
"hexColor",
|
|
"hexBgColor",
|
|
"italic",
|
|
"underline",
|
|
"strikethrough",
|
|
"monospace",
|
|
];
|
|
|
|
function prepare(text: string) {
|
|
return (
|
|
parseStyle(text)
|
|
// This optimizes fragments by combining them together when all their values
|
|
// for the properties defined above are equal.
|
|
.reduce((prev: ParsedStyle[], curr) => {
|
|
if (prev.length) {
|
|
const lastEntry = prev[prev.length - 1];
|
|
|
|
if (properties.every((key) => curr[key] === lastEntry[key])) {
|
|
lastEntry.text += curr.text;
|
|
lastEntry.end += curr.text.length;
|
|
return prev;
|
|
}
|
|
}
|
|
|
|
return prev.concat([curr]);
|
|
}, [])
|
|
);
|
|
}
|
|
|
|
export default prepare;
|