// Derived from Node.js v24.16.0 (MIT; Copyright Node.js contributors, and Joyent, Inc. and // other Node contributors). See LICENSE-node. // util.styleText, after Node v24's lib/util.js: SGR open/close pairs from inspect.colors // (aliases included), hex colours as 24-bit foregrounds, nested closes re-opened so an // inner style does not end an outer one, and no styling at all when the stream would not // show colours (Node's shouldColorize, through tty.ts). import { inspect } from "./inspect"; import { shouldColorize } from "./tty"; interface Style { openSeq: string; closeSeq: string; keepClose: boolean } const sgr = (n: number | string): string => `\u001b[${n}m`; /** Built from inspect.colors on first use, as Node caches it. Bold and dim share their * close (22), so closing one must not drop the other. */ let styles: Record | undefined; function styleCache(): Record { if (styles) return styles; styles = Object.create(null) as Record; const colors = inspect.colors as Record; for (const key of Object.getOwnPropertyNames(colors)) { const codes = colors[key]; if (codes) styles[key] = { openSeq: sgr(codes[0]), closeSeq: sgr(codes[1]), keepClose: codes[0] === 2 || codes[0] === 1 }; } return styles; } const hexColorRegExp = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; function replaceCloseCode(str: string, closeSeq: string, openSeq: string, keepClose: boolean): string { const closeLen = closeSeq.length; let index = str.indexOf(closeSeq); if (index === -1) return str; let result = ""; let lastIndex = 0; const replacement = keepClose ? closeSeq + openSeq : openSeq; do { const afterClose = index + closeLen; // A close at the very end is the inner style's own and stays. if (afterClose >= str.length) break; result += str.slice(lastIndex, index) + replacement; lastIndex = afterClose; index = str.indexOf(closeSeq, lastIndex); } while (index !== -1); return result + str.slice(lastIndex); } function hexToRgb(hex: string): [number, number, number] { const six = hex.length === 4 ? hex[1]! + hex[1] + hex[2] + hex[2] + hex[3] + hex[3] : hex.slice(1); return [0, 2, 4].map((at) => parseInt(six.slice(at, at + 2), 16)) as [number, number, number]; } function argType(name: string, expected: string, value: unknown): TypeError { const got = value === null || value === undefined ? `Received ${value}` : typeof value === "object" ? `Received an instance of ${(value as object).constructor?.name ?? "Object"}` : `Received type ${typeof value} (${typeof value === "string" ? `'${value}'` : String(value)})`; return Object.assign(new TypeError(`The "${name}" argument must be ${expected}. ${got}`), { code: "ERR_INVALID_ARG_TYPE" }); } function isStreamLike(s: unknown): boolean { if (typeof s !== "object" || s === null) return false; const o = s as Record; if (typeof ReadableStream !== "undefined" && (s instanceof ReadableStream || s instanceof WritableStream)) return true; return Boolean(o._readableState || o._writableState || (typeof o.write === "function" && typeof o.on === "function") || (typeof o.pipe === "function" && typeof o.on === "function")); } export function makeStyleText(stdout: () => unknown) { return function styleText( format: string | string[], text: string, options?: { validateStream?: boolean; stream?: unknown }, ): string { const validateStream = options?.validateStream ?? true; if (typeof text !== "string") throw argType("text", "of type string", text); if (options !== undefined && (typeof options !== "object" || options === null || Array.isArray(options))) { throw argType("options", "of type object", options); } if (typeof validateStream !== "boolean") throw argType("options.validateStream", "of type boolean", validateStream); let skipColorize = false; if (validateStream) { const stream = options?.stream ?? stdout(); if (!isStreamLike(stream)) throw argType("stream", "an instance of ReadableStream, WritableStream, or Stream", stream); skipColorize = !shouldColorize(stream); } let openCodes = ""; let closeCodes = ""; let processedText = text; for (const key of Array.isArray(format) ? format : [format]) { if (key === "none") continue; if (typeof key === "string" && hexColorRegExp.test(key)) { if (skipColorize) continue; const [r, g, b] = hexToRgb(key); const openSeq = sgr(`38;2;${r};${g};${b}`); const closeSeq = sgr(39); openCodes += openSeq; closeCodes = closeSeq + closeCodes; processedText = replaceCloseCode(processedText, closeSeq, openSeq, false); continue; } const style = typeof key === "string" ? styleCache()[key] : undefined; if (style === undefined) { if (typeof key === "string" && key[0] === "#") { throw Object.assign(new TypeError(`The argument 'format' must be a valid hex color (#RGB or #RRGGBB). Received ${inspect(key)}`), { code: "ERR_INVALID_ARG_VALUE", }); } const allowed = Object.getOwnPropertyNames(inspect.colors).map((k) => `'${k}'`).join(", "); throw Object.assign(new TypeError(`The argument 'format' must be one of: ${allowed}. Received ${inspect(key)}`), { code: "ERR_INVALID_ARG_VALUE", }); } openCodes += style.openSeq; closeCodes = style.closeSeq + closeCodes; processedText = replaceCloseCode(processedText, style.closeSeq, style.openSeq, style.keepClose); } if (skipColorize) return text; return `${openCodes}${processedText}${closeCodes}`; }; }