"use strict"; // 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"; // 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) { const result = []; 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) { return ( parseStyle(text) // This optimizes fragments by combining them together when all their values // for the properties defined above are equal. .reduce((prev, 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]); }, []) ); } module.exports = prepare;