interface ColorToken { type: "color" | "text"; content?: string; colorStart?: boolean; color?: string; } function parseColorTokens(message: string): ColorToken[] { const tokens: ColorToken[] = []; if (message) { let inColorStart = false; let inColorEnd = false; let chars: string[] = []; const messageLength = message.length; for (let i = 0; i < messageLength; i++) { const c = message.charAt(i); const nextC = (i + 1) < messageLength ? message.charAt(i + 1) : null; switch (c) { case "\\": if (nextC === null) { chars.push(c); } else { chars.push(nextC); i++; } break; case "[": if (inColorStart || inColorEnd) { // SHOULD NOT HAPPEN break; } else if (nextC == "/") { inColorEnd = true; i++; } else { inColorStart = true; } if (chars.length > 0) { tokens.push({ type: "text", content: chars.join(""), }); chars = []; } break; case "]": if (inColorStart || inColorEnd) { if (chars.length > 0) { tokens.push({ type: "color", colorStart: inColorStart, color: chars.join(""), }); chars = []; } inColorStart = false; inColorEnd = false; } else { chars.push(c); } break; default: chars.push(c); break; } } const inColor = inColorStart || inColorEnd; if (chars.length > 0 && !inColor) { tokens.push({ type: "text", content: chars.join(""), }); } } return tokens; } const COLOR_MAP: Record = { blink: "5", bold: "1", under: "4", red: "31", green: "32", yellow: "33", blue: "34", pink: "35", cyan: "36", }; function renderColorTokens(tokens: ColorToken[], renderColor: boolean): string { const text: string[] = []; const colorMapStack = new Map(); for (const token of tokens) { if (token.type === "color") { const color = token.color; if (color) { const colorCode = COLOR_MAP[color]; if (!colorCode) { text.push(`[${token.colorStart ? "" : "/"}${color}]`); continue; } const colorStack = colorMapStack.get(color) ?? []; if (colorStack.length == 0) { colorMapStack.set(color, colorStack); } if (token.colorStart) { colorStack.push(1); } else { colorStack.pop(); if (renderColor) { text.push("\x1b[0m"); } } const colors: string[] = []; for (const [color, colorStack] of colorMapStack) { if (colorStack.length > 0) { colors.push(colorCode); } } if (colors.length > 0) { if (renderColor) { text.push(`\x1b[${colors.join(";")}m`); } } } } else { if (token.content) { text.push(token.content); } } } if (renderColor) { text.push("\x1b[0m"); // FINALLY END ALL COLOR } return text.join(""); } function supportColor(): boolean { try { if (process.env.FORCE_COLOR !== undefined) { return process.env.FORCE_COLOR !== '0'; } if (process.env.NO_COLOR !== undefined) { return false; } return process.stdout.isTTY && process.stderr.isTTY; } catch (e) { // check color support failed, default false return false; } } export function renderColor(message: string, renderColor?: boolean): string { return renderColorTokens(parseColorTokens(message), renderColor ?? supportColor()); }