UNPKG

colorful-print

Version:

A flexible, chainable and lightweight colorful printing utility for Node.js

395 lines (360 loc) 17.6 kB
// colorful-print v1.0.1 by yacfish export const ANSIStyles = { reset: "\x1b[0m", black: "\x1b[30m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", white: "\x1b[37m", bright_black: "\x1b[90m", bright_red: "\x1b[91m", bright_green: "\x1b[92m", bright_yellow: "\x1b[93m", bright_blue: "\x1b[94m", bright_magenta: "\x1b[95m", bright_cyan: "\x1b[96m", bright_white: "\x1b[97m", bg_black: "\x1b[40m", bg_red: "\x1b[41m", bg_green: "\x1b[42m", bg_yellow: "\x1b[43m", bg_blue: "\x1b[44m", bg_magenta: "\x1b[45m", bg_cyan: "\x1b[46m", bg_white: "\x1b[47m", bg_bright_black: "\x1b[100m", bg_bright_red: "\x1b[101m", bg_bright_green: "\x1b[102m", bg_bright_yellow: "\x1b[103m", bg_bright_blue: "\x1b[104m", bg_bright_magenta: "\x1b[105m", bg_bright_cyan: "\x1b[106m", bg_bright_white: "\x1b[107m", bold: "\x1b[1m", underline: "\x1b[4m", italic: "\x1b[3m", inverse: "\x1b[7m", dim: "\x1b[2m", blink: "\x1b[5m", blink_fast: "\x1b[6m", hidden: "\x1b[8m", strikethrough: "\x1b[9m" }; class Printer { _options = {}; _instanceStyles = []; _isLn = false; _spacingConfig = { text: " ", useDefaultStyles: false, inheritStyle: false }; // --- Centralized Error Logging --- _logError(context, error) { try { // Use console.error for semantic correctness and potential separate stream handling const prefix = `${ANSIStyles.red}${ANSIStyles.underline}color-print error${ANSIStyles.reset}${ANSIStyles.red} [${context}]:\n${ANSIStyles.reset}`; const errorStack = error instanceof Error ? `${error.stack}` : ''; console.error(prefix + `${ANSIStyles.red}${errorStack}${ANSIStyles.reset}`); } catch (logError) { // Fallback if even error logging fails console.error("color-print internal error: Failed to log previous error.", logError); console.error("Original error context:", context); console.error("Original error:", error); } } constructor(isLnArg, optionsArg) { try { // Wrap constructor logic this._isLn = !!isLnArg; // Ensure options is a valid object if (typeof optionsArg !== 'object' || optionsArg === null) { if (optionsArg !== undefined) { // Log if non-object was passed, but not if just undefined this._logError('constructor', new TypeError(`Invalid options argument provided. Expected object, got ${typeof optionsArg}.`)); } this._options = {}; } else { this._options = optionsArg; } // Validate and set default styles const defaultStylesSource = this._options.defaultStyles; if (defaultStylesSource && !Array.isArray(defaultStylesSource)) { this._logError('constructor', new TypeError(`Invalid 'defaultStyles' option. Expected array, got ${typeof defaultStylesSource}.`)); this._instanceStyles = []; } else { this._instanceStyles = defaultStylesSource ? [...defaultStylesSource] : []; } // Validate and set spacing options try { const spacingOpt = this._options.spacing; this._spacingConfig = { text: (spacingOpt && typeof spacingOpt.text === 'string') ? spacingOpt.text : " ", useDefaultStyles: (spacingOpt && spacingOpt.useDefaultStyles === true), inheritStyle: (spacingOpt && spacingOpt.inheritStyle === true && !(spacingOpt.useDefaultStyles === true)) }; if (spacingOpt && typeof spacingOpt !== 'object') { throw new TypeError(`Invalid 'spacing' option. Expected object, got ${typeof spacingOpt}.`); } } catch(e) { this._logError('constructor spacing options', e); this._spacingConfig = { text: " ", useDefaultStyles: false, inheritStyle: false }; // Reset to default on error } // Validate and attach custom styles const customStylesSource = this._options.customStyles; if (customStylesSource) { // Only process if it exists if (typeof customStylesSource !== 'object' || customStylesSource === null) { this._logError('constructor', new TypeError(`Invalid 'customStyles' option. Expected object, got ${typeof customStylesSource}.`)); } else { try { // Wrap the loop itself const customStyleKeys = Object.keys(customStylesSource); customStyleKeys.forEach(customStyleName => { const styleCode = customStylesSource[customStyleName]; if (typeof styleCode !== 'string' || styleCode === '') { // Log warning instead of error for invalid individual styles console.warn(`${ANSIStyles.yellow}color-print warning: Invalid or empty code for custom style '${customStyleName}'. Skipping.${ANSIStyles.reset}`); return; // Skip this style } if (typeof this[customStyleName] === 'undefined') { this[customStyleName] = function(...args) { // Add try-catch within the method call itself for safety try { return this._applyStyleInternal(styleCode, ...args); } catch(e) { this._logError(`custom style '${customStyleName}' execution`, e); return undefined; // Prevent crashing chain on style method error } }; } else { console.warn(`${ANSIStyles.yellow}color-print warning: Custom style '${customStyleName}' conflicts with existing property. Skipping.${ANSIStyles.reset}`); } }); } catch (e) { this._logError('attaching custom styles', e); } } } // Validate typeHandlers (check if values are functions) const typeHandlersSource = this._options.typeHandlers; if (typeHandlersSource) { if (typeof typeHandlersSource !== 'object' || typeHandlersSource === null) { this._logError('constructor', new TypeError(`Invalid 'typeHandlers' option. Expected object, got ${typeof typeHandlersSource}.`)); } else { Object.keys(typeHandlersSource).forEach(key => { if (typeof typeHandlersSource[key] !== 'function') { this._logError('constructor typeHandlers', new TypeError(`Invalid handler for type '${key}'. Expected function, got ${typeof typeHandlersSource[key]}. Handler ignored.`)); delete typeHandlersSource[key]; // Remove invalid handler } }); } } } catch (e) { // Catch errors during initial setup this._logError('constructor critical setup', e); // Ensure basic defaults exist even if setup failed, to prevent later crashes on method calls this._options = {}; this._instanceStyles = []; this._isLn = false; this._spacingConfig = { text: " ", useDefaultStyles: false, inheritStyle: false }; } // --- Create and return wrapper --- // This part should be safe if constructor logic above handles errors gracefully const instance = this; const printer = (...args) => { try { if (args.length > 0) { instance._printInternal(...args); } else if (instance._isLn) { // Use internal method for consistency if needed, but console.log is likely safe console.log(""); } // Create clean instance *carefully* const nextPrinter = instance._createCleanInstanceInternal(); return nextPrinter; } catch(e) { instance._logError('wrapper function', e); // Return a non-crashing dummy function maybe? Or just undefined. // Returning undefined clearly breaks the chain. return undefined; } }; Object.setPrototypeOf(printer, instance); return printer; } _createCleanInstanceInternal() { try { // Wrap instance creation // Pass the potentially cleaned/validated options reference const newWrapper = new Printer(this._isLn, this._options); return newWrapper; } catch(e) { this._logError('_createCleanInstanceInternal', e); // Return a dummy/broken instance? Or undefined? return undefined; // Break chain on creation error } } _createChainedInstanceInternal() { try { // Wrap instance creation const ln = this._isLn; const opts = this._options; // Pass validated options reference const newWrapper = new Printer(ln, opts); const newInstance = Object.getPrototypeOf(newWrapper); // Copy styles state (safe operation) newInstance._instanceStyles = [...this._instanceStyles]; return newWrapper; } catch(e) { this._logError('_createChainedInstanceInternal', e); return undefined; // Break chain on creation error } } _printInternal(...args) { let didErrorOccur = false; // Flag to prevent style reset if error happened mid-print try { const currentOptions = this._options; const spacingConfig = this._spacingConfig; const defaultStyles = Array.isArray(currentOptions.defaultStyles) ? currentOptions.defaultStyles : []; if (args.length === 1 && args[0] === "" && this._isLn) { console.log(""); // Style reset happens in finally block return; } const typeHandlers = currentOptions.typeHandlers || {}; const formatted = args.map(arg => { let processedArg = null; try { // Wrap individual argument processing const type = arg === null ? "null" : Array.isArray(arg) ? "array" : typeof arg; const handler = typeHandlers.hasOwnProperty(type) ? typeHandlers[type] : null; if (handler) { processedArg = handler(arg); // User-provided function // Validate handler output if (!processedArg || typeof processedArg.text !== 'string') { throw new TypeError(`Type handler for type '${type}' returned invalid value. Expected { text: string, styles?: string[] }.`); } // Use provided styles or fallback to current instance styles processedArg.styles = Array.isArray(processedArg.styles) ? processedArg.styles : (this._instanceStyles || []); } else { // Default: convert to string and use current styles processedArg = { text: String(arg), styles: this._instanceStyles || [] }; } } catch (e) { didErrorOccur = true; this._logError(`processing argument (type: ${typeof arg})`, e); // Fallback formatting on error processedArg = { text: `[ERROR processing value: ${String(arg).substring(0, 50)}${String(arg).length > 50 ? '...' : ''}]`, styles: [ANSIStyles.red] // Basic error style }; } // Ensure styles is always an array post-processing/catch processedArg.styles = Array.isArray(processedArg.styles) ? processedArg.styles : []; return processedArg; }); const nonEmptyFormatted = formatted.filter(f => f.text !== ""); let outputParts = []; for (let i = 0; i < nonEmptyFormatted.length; i++) { const currentFormatted = nonEmptyFormatted[i]; outputParts.push(`${currentFormatted.styles.join("")}${currentFormatted.text}${ANSIStyles.reset}`); if (i < nonEmptyFormatted.length - 1) { let separatorStyles = []; if (spacingConfig.useDefaultStyles) { separatorStyles = defaultStyles; } else if (spacingConfig.inheritStyle) { separatorStyles = currentFormatted.styles; } // Join safely, ensuring array elements are strings const joinedSepStyles = Array.isArray(separatorStyles) ? separatorStyles.join("") : ""; outputParts.push(`${joinedSepStyles}${spacingConfig.text}${ANSIStyles.reset}`); } } const output = outputParts.join(""); try { // Wrap console output specifically if (output || this._isLn) { if (this._isLn) console.log(ANSIStyles.reset+output); else if (output) process.stdout.write(ANSIStyles.reset+output); } } catch (e) { didErrorOccur = true; this._logError('console output', e); } } catch (e) { // Catch broader errors in _printInternal didErrorOccur = true; this._logError('_printInternal', e); } finally { // Always reset styles *unless* an error occurred mid-way through this specific print call // This prevents a partial print leaving the instance in a weird state for the *next* call if (!didErrorOccur) { try { this._instanceStyles = Array.isArray(this._options?.defaultStyles) ? [...this._options.defaultStyles] : []; } catch(e) { // If even resetting styles fails, log it but don't crash this._logError('resetting styles in finally', e); this._instanceStyles = []; // Hard reset } } } } _applyStyleInternal(styleCode, ...args) { try { // Wrap the entire style application logic if (typeof styleCode !== 'string' || styleCode === '') { // This should ideally be caught earlier, but added as safety throw new TypeError('Invalid styleCode received by _applyStyleInternal.'); } const nextWrapper = this._createChainedInstanceInternal(); // If creation failed, nextWrapper will be undefined if (!nextWrapper) { throw new Error("Failed to create chained instance in _applyStyleInternal."); } const nextInstance = Object.getPrototypeOf(nextWrapper); nextInstance._instanceStyles.push(styleCode); if (args.length > 0) { // If args are provided, print immediately using the new state const result = nextWrapper(...args); // If the wrapper call itself failed, result might be undefined return result; } else { // If no args, return the new wrapper for further chaining return nextWrapper; } } catch(e) { this._logError('_applyStyleInternal', e); // Return undefined to explicitly break the chain on error return undefined; } } } // --- Add standard style methods to prototype --- // Wrap these calls as well for extra safety Object.keys(ANSIStyles).forEach(styleName => { if (styleName !== 'reset') { Printer.prototype[styleName] = function(...args) { try { return this._applyStyleInternal(ANSIStyles[styleName], ...args); } catch (e) { // Log error originating from standard style method call this._logError(`standard style '${styleName}'`, e); return undefined; // Break chain } }; } }); // --- RGB/256 methods --- // Wrap these too Printer.prototype.rgb = function(r, g, b, ...args) { try { return this._applyStyleInternal(`\x1b[38;2;${r};${g};${b}m`, ...args); } catch(e) { this._logError('rgb method', e); return undefined; } }; Printer.prototype.bg_rgb = function(r, g, b, ...args) { try { return this._applyStyleInternal(`\x1b[48;2;${r};${g};${b}m`, ...args); } catch(e) { this._logError('bg_rgb method', e); return undefined; } }; Printer.prototype.fg = function(color, ...args) { try { return this._applyStyleInternal(`\x1b[38;5;${color}m`, ...args); } catch(e) { this._logError('fg method', e); return undefined; } }; Printer.prototype.bg = function(color, ...args) { try { return this._applyStyleInternal(`\x1b[48;5;${color}m`, ...args); } catch(e) { this._logError('bg method', e); return undefined; } }; // --- Export concrete classes --- // Constructors inherently call the wrapped Printer constructor export class PrintLn extends Printer { constructor(options) { super(true, options); } } export class Print extends Printer { constructor(options) { super(false, options); } }