|
const colorCodes = { |
|
/** Strips ANSI color codes from a string. */ |
|
uncolorize: (str: string) => str.replace(/(\x1b|)\[[\d;]*m/gi, ''), // eslint-disable-line no-control-regex |
|
|
|
reset: '0', |
|
bright: '1', |
|
dim: '2', |
|
underscore: '4', |
|
blink: '5', |
|
reverse: '7', |
|
hidden: '8', |
|
|
|
fg: { |
|
black: '30', |
|
red: '31', |
|
green: '32', |
|
yellow: '33', |
|
blue: '34', |
|
magenta: '35', |
|
cyan: '36', |
|
white: '37', |
|
crimson: '38', |
|
}, |
|
|
|
bg: { |
|
black: '40', |
|
red: '41', |
|
green: '42', |
|
yellow: '43', |
|
blue: '44', |
|
magenta: '45', |
|
cyan: '46', |
|
white: '47', |
|
crimson: '48', |
|
}, |
|
|
|
/** e.g. `fold('0;30;41')` => `\x1b[0;30;41m` */ |
|
fold(codeString: CodeString) { return `\x1b[${codeString as string}m` as const }, |
|
} as const |
|
|
|
/* Proxy implementation */ |
|
|
|
const trap = { |
|
get(target: Secret & ProxyTarget, prop: ColorizerKeys & ProxyHandler<ProxyTarget>, receiver: typeof target) { |
|
|
|
// Proxify own properties |
|
if (Object.hasOwn(target, prop)) { |
|
let _code: CodeString = target._code ?? '' |
|
|
|
let nextTarget: ProxyTarget |
|
let callable: CallableTaggedTemplateProxy | object |
|
|
|
if (['fg', 'bg'].includes(prop)) { |
|
nextTarget = colorCodes[prop as 'fg' | 'bg'] |
|
callable = {} |
|
} else { |
|
nextTarget = colorCodes |
|
_code += (_code ? ';' : '') + (target as ColorCodes)[prop as keyof ColorCodes] |
|
callable = function callableTaggedTemplate(...message: Args): string { |
|
// If a message is provided, wrap it between `color` and `reset` codes -> _.bg.red`message` -> \x1b[41mmessage\x1b[0m |
|
// or return just the color code to allow manual composition. -> _.bg.red + myString + _.reset -> \x1b[41m + myString + \x1b[0m |
|
return colorCodes.fold(_code) + (message.length ? x(message) + colorCodes.fold(colorCodes.reset) : '') |
|
} |
|
} |
|
|
|
const newTarget = Object.assign(callable, nextTarget, { _code }) |
|
return new Proxy(newTarget, trap) |
|
} |
|
|
|
// Override default behavior |
|
if (['toString', 'valueOf', Symbol.toPrimitive].includes(prop)) |
|
return () => colorCodes.fold(target._code) |
|
|
|
// Support string methods directly on the proxy |
|
if (String.prototype[prop as keyof string] !== undefined) |
|
return String.prototype[prop as keyof string] |
|
|
|
// Loop back on unknown properties, e.g. _.bg.unknown.fg.black.yellow.fg.dim.red.bright.white.underscore => _.bg.black.fg.red.bright.underscore |
|
// Explanation: after .bg it loops until receiving a valid color name, same after .black where it expects root level property, etc. |
|
return receiver |
|
}, |
|
set() { console.warn("Proxy Colorizer: Assignment isn't supported."); return true }, // Silently ignore sets |
|
} |
|
|
|
/** |
|
* @author Qwerty <[email protected]> |
|
* @link [gist](https://gist.github.com/ackvf/68f992660a5eda645c4671d3599b2acf#file-colorizer-proxy-ts) |
|
* |
|
* @description An extension to the console coloring object using proxy with cleaner syntax, chaining, tagged template literal support & customizations. |
|
* |
|
* note: |
|
* - `\x1b` = `` |
|
* - joining supported: `\x1b[0;30;41m` |
|
* |
|
* @example |
|
* |
|
* const red = colorProxy.fg.red |
|
* const dyeRed = colorProxy.fg.black.bg.red |
|
* |
|
* console.log(red`Hallo?`) |
|
* console.log(dyeRed('ERROR:')) |
|
* |
|
* logger.log(`${colors.fg.yellow}API server is listening on http://localhost:${red(port)+colors.fg.yellow}/api/v1/...${colors.reset}`) |
|
* |
|
* @example // All these are equivalent. |
|
* |
|
* console.log(colorProxy.fg.black.bg.red + 'Is this thing on?' + colors.reset + ' ...') |
|
* console.log(colorProxy.fg.black.bg.red('Is this thing on?') + ' ...') |
|
* console.log(colorProxy.fg.black.bg.red`Is this thing on?` + ' ...') |
|
* |
|
* @example |
|
* console.log('\x1b[32;41m green-red') |
|
* console.log('[32;41m green-red') |
|
* console.log(c.uncolorize('[32;41m default')) |
|
*/ |
|
|
|
export const colorProxy = new Proxy<ColorProxy>(colorCodes as ColorProxy, trap as ProxyHandler<ColorProxy>) |
|
export default colorProxy |
|
|
|
|
|
/* Support for template literals */ |
|
|
|
function interlace(strs: TemplateStringsArray | TemplateStringsArray['raw'], ...args: TextLike[]): string { |
|
return strs.reduce((prev, current, ix) => prev + (current ?? '') + (args[ix] ?? ''), '') |
|
} |
|
|
|
const x = extractMessage |
|
function extractMessage(args: Args): string { |
|
if (!['string', 'number', 'boolean'].includes(typeof args[0]) && !Array.isArray(args[0])) return '' |
|
if (isTemplate(args)) return interlace(...args) |
|
return `${args.join(' ') ?? ''}` |
|
} |
|
|
|
function isTemplate(args: Args): args is TemplateArgs { return !!(args as TemplateArgs)?.[0]?.raw } |
|
|
|
type Args = |
|
| /** e.g. green('yes') */ MsgArgs |
|
| /** e.g. red`no` */ TemplateArgs |
|
/** When called as a tagged template: `` red`nope` `` */ |
|
type TemplateArgs = [template: TemplateStringsArray, ...values: TextLike[]] |
|
/** When called as a function: `` green('yes') `` */ |
|
type MsgArgs = [message: TextLike] |
|
|
|
type TextLike = string | number | boolean |
|
|
|
/* Support for Proxy */ |
|
|
|
export type ColorProxy = |
|
& Secret |
|
& Utils |
|
& Modifiers |
|
& { |
|
fg: Secret & { [key in keyof ColorCodes['fg']]: CallableTaggedTemplateProxy } |
|
bg: Secret & { [key in keyof ColorCodes['bg']]: CallableTaggedTemplateProxy } |
|
} |
|
|
|
type CallableTaggedTemplateProxy = string & ColorProxy & ((...msg: Args) => string) |
|
|
|
type ColorCodes = typeof colorCodes |
|
type Secret = { _code: CodeString } |
|
type Utils = Pick<ColorCodes, 'uncolorize' | 'fold'> |
|
type Modifiers = { [key in Extract<keyof ColorCodes, 'reset' | 'bright' | 'dim' | 'underscore' | 'blink' | 'reverse' | 'hidden'>]: CallableTaggedTemplateProxy } |
|
|
|
type CodeString = `${number}${`;${number}${`;${number}${string}` | ''}` | ''}` | '' |
|
|
|
type ProxyTarget = ColorCodes | ColorCodes['fg' | 'bg'] |
|
type ColorizerKeys = keyof ColorCodes | keyof ColorCodes['bg' | 'fg'] |
|
|
|
// --------------------------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* ## Convenience methods |
|
* |
|
* - as functions `` red(greet + "world") `` |
|
* - as tagged templates `` red`${greet} world` `` |
|
* - chainable `` dyeRed = _.bg.red.fg.black `` |
|
* - composable `` _.bg.red + _.fg.black + "hello" + _.reset `` or `` bgRed + fgBlack + "hello" + _.reset `` |
|
*/ |
|
|
|
export const _ = colorProxy |
|
|
|
export const dyeRed = _.fg.black.bg.red |
|
export const dyeGreen = _.fg.black.bg.green |
|
export const dyeBlue = _.fg.black.bg.blue |
|
|
|
export const red = _.fg.red |
|
export const green = _.fg.green |
|
export const blue = _.fg.blue |
|
|
|
export const r = _.fg.red |
|
export const g = _.fg.green |
|
export const b = _.fg.blue |
|
export const c = _.fg.cyan |
|
export const m = _.fg.magenta |
|
export const y = _.fg.yellow |
|
export const k = _.fg.black |
|
export const w = _.fg.white |
|
|
|
export const R = _.fg.black.bg.red |
|
export const G = _.fg.black.bg.green |
|
export const B = _.fg.black.bg.blue |
|
export const C = _.fg.black.bg.cyan |
|
export const M = _.fg.black.bg.magenta |
|
export const Y = _.fg.black.bg.yellow |
|
export const W = _.fg.black.bg.white |