UNPKG

nhb-toolbox

Version:

A versatile collection of smart, efficient, and reusable utility functions, classes and types for everyday development needs.

287 lines (286 loc) 10.6 kB
var _a; import { convertHexToRgb, convertHslToRgb } from '../colors/convert.js'; import { CSS_COLORS } from '../colors/css-colors.js'; import { isHex6, isRGB } from '../colors/guards.js'; import { _isValidHue, _isValidPercentage, _isValidRGBComponent } from '../colors/helpers.js'; import { isNumber, isString } from '../guards/primitives.js'; import { isBrowser } from '../guards/specials.js'; import { _logToConsole } from './console.log.js'; import { ANSI_16_COLORS, ANSI_TEXT_STYLES, CSS_TEXT_STYLES } from './constants.js'; import { _css16ToHex, _extractColorName, _isAnsi16ColorValue, _isAnsiSequence, _isCSS16Color, } from './helpers.js'; export function detectColorSupport() { if ('NO_COLOR' in process.env) return 0; if ('FORCE_COLOR' in process.env) return 3; if (!process.stdout.isTTY) return 0; const term = process.env.TERM ?? process.env.COLORTERM ?? ''; if (term === 'dumb') return 0; if (/\b256(color)?\b/i.test(term)) return 2; if (/\btruecolor\b|\b24bit\b/i.test(term)) return 3; return 1; } export function rgbToAnsi(r, g, b, isBg = false) { const open = `\x1b[${isBg ? 48 : 38};2;${r};${g};${b}m`; const close = `\x1b[${isBg ? 49 : 39}m`; return [open, close]; } export function hexToAnsi(hex, isBg = false) { const rgb = (convertHexToRgb(hex).match(/\d+/g) || []).map(parseFloat); return rgbToAnsi(...rgb, isBg); } export function isCSSColor(value) { return value in CSS_COLORS; } export function isBGColor(value) { return value?.startsWith('bg') && isCSSColor(value.slice(2).toLowerCase()); } export function isTextStyle(value) { return value in CSS_TEXT_STYLES || value in ANSI_TEXT_STYLES; } export class LogStyler { #styles; constructor(styles = []) { this.#styles = styles; } #applyStyles(...style) { return createStylogProxy(new _a([...this.#styles, ...style])); } style(...style) { return this.#applyStyles(...style); } ansi16(color) { return this.#applyStyles(ANSI_16_COLORS[color], `css-${color}`); } toCSS(input, stringify = false) { const stringified = stringify === true ? JSON.stringify(input) : `${input}`; const cssList = []; for (const style of this.#styles) { if (isString(style)) { if (isTextStyle(style)) { cssList.push(CSS_TEXT_STYLES[style]); } else if (isBGColor(style)) { const color = CSS_COLORS[_extractColorName(style)]; cssList.push(`background: ${color}`); } else if (isCSSColor(style)) { const color = CSS_COLORS[style]; cssList.push(`color: ${color}`); } else if (this.#isValidHexOrRGB(style)) { if (style.startsWith('bg-')) { cssList.push(`background: ${style?.replace('bg-', '')}`); } else { cssList.push(`color: ${style}`); } } else if (_isCSS16Color(style)) { const color = _css16ToHex(style); const colorValue = style.startsWith('css-bg') ? `background: ${color}` : `color: ${color}`; cssList.push(colorValue); } } } return [`%c${stringified}`, cssList]; } toANSI(input, stringify = false) { const stringified = stringify === true ? JSON.stringify(input) : `${input}`; let openSeq = '', closeSeq = ''; let fgOpenSeq = '', bgOpenSeq = ''; const reopenSequences = new Map(); for (const style of this.#styles) { if (isString(style)) { if (isTextStyle(style)) { const [open, close] = ANSI_TEXT_STYLES[style]; openSeq += open; closeSeq = close + closeSeq; reopenSequences.set(close, (reopenSequences.get(close) ?? '') + open); } else if (isBGColor(style)) { const hex = CSS_COLORS[_extractColorName(style)]; const [open, close] = hexToAnsi(hex, true); openSeq += open; closeSeq = close + closeSeq; bgOpenSeq = open; } else if (isCSSColor(style)) { const hex = CSS_COLORS[style]; const [open, close] = hexToAnsi(hex, false); openSeq += open; closeSeq = close + closeSeq; fgOpenSeq = open; } } else if (_isAnsiSequence(style)) { openSeq += style[0]; closeSeq = style[1] + closeSeq; if (style[1] === '\x1b[49m') { bgOpenSeq = style[0]; } else if (style[1] === '\x1b[39m') { fgOpenSeq = style[0]; } } else if (_isAnsi16ColorValue(style)) { const [open, close] = style.map((s) => `\x1b[${s}m`); openSeq += open; closeSeq = close + closeSeq; if (close === '\x1b[49m') { bgOpenSeq = open; } else if (close === '\x1b[39m') { fgOpenSeq = open; } } } if (!detectColorSupport()) { return stringified; } else { let nestedStr = stringified; if (nestedStr.includes('\x1b[')) { if (fgOpenSeq) { nestedStr = nestedStr.replaceAll('\x1b[39m', `\x1b[39m${fgOpenSeq}`); } if (bgOpenSeq) { nestedStr = nestedStr.replaceAll('\x1b[49m', `\x1b[49m${bgOpenSeq}`); } for (const [close, reopen] of reopenSequences) { nestedStr = nestedStr.replaceAll(close, `${close}${reopen}`); } if (openSeq) { nestedStr = nestedStr.replaceAll('\x1b[0m', `\x1b[0m${openSeq}`); } } return openSeq.concat(nestedStr, closeSeq); } } log(input, stringify = false) { if (isBrowser()) { const [fmt, cssList] = this.toCSS(input, stringify); _logToConsole(fmt, cssList.join(';')); } else { _logToConsole(this.toANSI(input, stringify)); } } #isValidHexOrRGB(color) { const pure = color?.replace('bg-', ''); return isHex6(pure) || isRGB(pure); } #sanitizeHex(code) { return code?.trim()?.startsWith('#') ? code?.trim() : `#${code?.trim()}`; } #handleHex(code, isBg = false) { const sanitized = this.#sanitizeHex(code); if (!isHex6(sanitized)) { return this.#applyStyles(); } const ansi = hexToAnsi(sanitized, isBg); return this.#applyStyles(isBg ? `bg-${sanitized}` : sanitized, ansi); } hex(code) { return this.#handleHex(code, false); } bgHex(code) { return this.#handleHex(code, true); } #extractColorValues(code) { const trimmed = code?.trim(); return (trimmed?.match(/[\d.]+%?/g) || []).map(parseFloat); } #isValidRGB(...value) { return value.every(_isValidRGBComponent); } #handleRGB(code, green, blue, isBg = false) { if (isString(code)) { const rgb = this.#extractColorValues(code); if (this.#isValidRGB(...rgb)) { return this.#applyStyles(rgbToAnsi(...rgb, isBg), isBg ? `bg-rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})` : `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`); } else { return this.#applyStyles(); } } else if (isNumber(code) && isNumber(green) && isNumber(blue)) { if (this.#isValidRGB(code, green, blue)) { return this.#applyStyles(rgbToAnsi(code, green, blue, isBg), isBg ? `bg-rgb(${code}, ${green}, ${blue})` : `rgb(${code}, ${green}, ${blue})`); } else { return this.#applyStyles(); } } else { return this.#applyStyles(); } } rgb(code, green, blue) { return this.#handleRGB(code, green, blue, false); } bgRGB(code, green, blue) { return this.#handleRGB(code, green, blue, true); } #isValidHSL(h, s, l) { return _isValidHue(h) && _isValidPercentage(s) && _isValidPercentage(l); } #handleHSL(code, saturation, lightness, isBg = false) { if (isString(code)) { const hsl = this.#extractColorValues(code); if (this.#isValidHSL(...hsl)) { return this.#handleRGB(convertHslToRgb(...hsl), undefined, undefined, isBg); } else { return this.#applyStyles(); } } else if (isNumber(code) && isNumber(saturation) && isNumber(lightness)) { if (this.#isValidHSL(code, saturation, lightness)) { return this.#handleRGB(convertHslToRgb(code, saturation, lightness), undefined, undefined, isBg); } else { return this.#applyStyles(); } } else { return this.#applyStyles(); } } hsl(code, saturation, lightness) { return this.#handleHSL(code, saturation, lightness, false); } bgHSL(code, saturation, lightness) { return this.#handleHSL(code, saturation, lightness, true); } } _a = LogStyler; function createStylogProxy(styler) { return new Proxy(styler, { get(target, prop) { if (prop in target) { const value = target[prop]; if (typeof value === 'function') { return value.bind(target); } else { return value; } } if (isCSSColor(prop) || isBGColor(prop) || isTextStyle(prop)) { return createStylogProxy(target.style(prop)); } }, }); } export const Stylog = createStylogProxy(new LogStyler());