import {
LoggerFactory,
LoggingConfig,
LoggingContext,
LoggingFilter,
LogMeta,
StringLike,
Theme,
ThemeOption,
ThemeOptionByLogLevel,
Logger,
} from "./types";
import { ColorizeOptions, style, StyledString } from "styled-string-builder";
import { DefaultTheme, LogLevel, NumericLogLevels } from "./constants";
import { sf } from "./text";
import { LoggedEnvironment } from "./environment";
import { getObjectName, isClass, isFunction, isInstance } from "./utils";
export const ROOT_CONTEXT_SYMBOL = Symbol("MiniLoggerRootContext");
/**
* @description A minimal logger implementation.
* @summary MiniLogger is a lightweight logging class that implements the Logger interface. It provides basic logging functionality with support for different log levels, verbosity, context-aware logging, and customizable formatting.
* @param {string} [context] - The context (typically class name) this logger is associated with.
* @param {Partial<LoggingConfig>} [conf] - Optional configuration to override global settings.
* @param {string[]} [baseContext=[]] - The base context for the logger.
* @class MiniLogger
* @example
* // Create a new logger for a class
* const logger = new MiniLogger('MyClass');
*
* // Log messages at different levels
* logger.info('This is an info message');
* logger.debug('This is a debug message');
* logger.error('Something went wrong');
*
* // Create a child logger for a specific method
* const methodLogger = logger.for('myMethod');
* methodLogger.verbose('Detailed information', 2);
*
* // Log with custom configuration
* logger.for('specialMethod', { style: true }).info('Styled message');
*/
export class MiniLogger implements Logger {
protected context: string[];
protected baseContext: string[];
constructor(
context?: string,
protected conf?: Partial<LoggingConfig>,
baseContext: string[] = []
) {
this.baseContext = Array.isArray(baseContext) ? [...baseContext] : [];
if (context) this.baseContext.push(context);
this.context = [...this.baseContext];
(this as any)[ROOT_CONTEXT_SYMBOL] = [...this.baseContext];
}
protected config(
key: keyof LoggingConfig
): LoggingConfig[keyof LoggingConfig] {
if (this.conf && key in this.conf) return this.conf[key];
return Logging.getConfig()[key];
}
for(config: Partial<LoggingConfig>): this;
for(
method:
| string
| ((...args: any[]) => any)
| { new (...args: any[]): any }
| object
): this;
for(
method:
| string
| ((...args: any[]) => any)
| { new (...args: any[]): any }
| object
| Partial<LoggingConfig>,
config: Partial<LoggingConfig>,
...args: any[]
): this;
/**
* @description Creates a child logger for a specific method or context.
* @summary Returns a new logger instance with the current context extended by the specified method name.
* @param {string | Function | object | Partial<LoggingConfig>} [method] - The method name, function, or configuration to create a logger for.
* @param {Partial<LoggingConfig>} [config] - Optional configuration to override settings.
* @param {...any[]} args - Additional arguments to pass to the logger factory.
* @return {Logger} A new logger instance for the specified method.
*/
for(
method?:
| string
| ((...args: any[]) => any)
| { new (...args: any[]): any }
| object
| Partial<LoggingConfig>,
config?: Partial<LoggingConfig>,
...args: any[] // eslint-disable-line @typescript-eslint/no-unused-vars
): this {
let contextName: string | undefined;
let childConfig = config;
const parentContext = Array.isArray(this.context)
? [...this.context]
: typeof this.context === "string" && this.context
? [this.context]
: [];
const rootCandidate = (this as any)[ROOT_CONTEXT_SYMBOL];
const baseContext = Array.isArray(rootCandidate)
? [...rootCandidate]
: Array.isArray(this.baseContext)
? [...this.baseContext]
: [];
if (typeof method === "string") {
contextName = method;
} else if (method !== undefined) {
if (isClass(method) || isInstance(method) || isFunction(method)) {
contextName = getObjectName(method);
} else if (!childConfig && method && typeof method === "object") {
childConfig = method as Partial<LoggingConfig>;
}
}
let contextSegments = contextName
? [...parentContext, contextName]
: [...parentContext];
return new Proxy(this, {
get: (target: typeof this, p: string | symbol, receiver: any) => {
const result = Reflect.get(target, p, receiver);
if (p === "config") {
return new Proxy(this.config, {
apply: (
target: typeof this.config,
_thisArg: unknown,
argArray: [keyof LoggingConfig]
) => {
const [key] = argArray;
if (childConfig && key !== undefined && key in childConfig) {
return childConfig[key];
}
return Reflect.apply(target, receiver, argArray);
},
get: (target: typeof this.config, key: string | symbol) => {
if (childConfig && key in childConfig)
return childConfig[key as keyof LoggingConfig];
return Reflect.get(target, key, receiver);
},
});
}
if (p === "clear") {
return () => {
contextSegments = [...baseContext];
childConfig = undefined;
return receiver;
};
}
if (p === "context") {
return contextSegments;
}
if (p === "root") {
return [...baseContext];
}
if (p === ROOT_CONTEXT_SYMBOL) {
return baseContext;
}
if (p === "for") {
return (...innerArgs: Parameters<MiniLogger["for"]>) => {
const originalContext = Array.isArray(target.context)
? [...target.context]
: typeof target.context === "string" && target.context
? [target.context]
: [];
target.context = [...contextSegments];
try {
// eslint-disable-next-line prefer-spread
return target.for.apply(target, innerArgs);
} finally {
target.context = originalContext;
}
};
}
return result;
},
}) as this;
}
protected getConfigSnapshot(): LoggingConfig {
return {
...Logging.getConfig(),
...(this.conf || {}),
} as LoggingConfig;
}
protected getContextSegments(): string[] {
if (Array.isArray(this.context)) return [...this.context];
if (typeof this.context === "string" && this.context) return [this.context];
return [];
}
protected resolveFilters(config: LoggingConfig): LoggingFilter[] {
const candidate = config.filters;
if (!Array.isArray(candidate)) return [];
return candidate.filter(
(entry): entry is LoggingFilter =>
typeof entry === "object" &&
entry !== null &&
typeof (entry as LoggingFilter).filter === "function"
);
}
protected applyFilters(
message: string,
context: string[],
config: LoggingConfig
): string {
const filters = this.resolveFilters(config);
if (!filters.length) return message;
return filters.reduce((current, filter) => {
try {
const next = filter.filter(config, current, [...context]);
return typeof next === "string" ? next : current;
} catch {
return current;
}
}, message);
}
/**
* @description Creates a formatted log string.
* @summary Generates a log string with timestamp, colored log level, context, and message.
* @param {LogLevel} level - The log level for this message.
* @param {StringLike | Error} message - The message to log or an Error object.
* @param {Error} [error] - Optional error to extract stack trace to include in the log.
* @return {string} A formatted log string with all components.
*/
protected createLog(
level: LogLevel,
message: StringLike | Error,
error?: Error,
meta?: LogMeta
): string {
const log: Record<
| "timestamp"
| "level"
| "context"
| "correlationId"
| "message"
| "separator"
| "stack"
| "app"
| "meta",
string | LogMeta
> = {} as any;
const style = this.config("style");
const separator = this.config("separator");
const app = Logging.getConfig().app;
if (app) log.app = style ? Logging.theme(app as string, "app", level) : app;
if (separator)
log.separator = style
? Logging.theme(separator as string, "separator", level)
: (separator as string);
if (this.config("timestamp")) {
const date = new Date().toISOString();
const timestamp = style ? Logging.theme(date, "timestamp", level) : date;
log.timestamp = timestamp;
}
if (this.config("logLevel")) {
const lvl: string = style
? Logging.theme(level, "logLevel", level)
: level;
log.level = lvl.toUpperCase();
}
if (this.config("context")) {
const contextSegments = Array.isArray(this.context)
? this.context
: typeof this.context === "string" && this.context
? [this.context]
: [];
if (contextSegments.length) {
const joined = contextSegments.join(
(this.config("contextSeparator") as string) || "."
);
const context = style ? Logging.theme(joined, "class", level) : joined;
log.context = context;
}
}
if (this.config("correlationId")) {
{
const id: string = style
? Logging.theme(this.config("correlationId")!.toString(), "id", level)
: this.config("correlationId")!.toString();
log.correlationId = id;
}
}
const configSnapshot = this.getConfigSnapshot();
const contextSegments = this.getContextSegments();
const rawMessage =
typeof message === "string" ? message : (message as Error).message;
const filteredMessage = this.applyFilters(
rawMessage,
contextSegments,
configSnapshot
);
const msg: string = style
? Logging.theme(filteredMessage, "message", level)
: filteredMessage;
log.message = msg;
const showMeta = Boolean(this.config("meta"));
const metaPayload = showMeta && meta ? meta : undefined;
const metaString = metaPayload ? this.formatMeta(metaPayload) : undefined;
const filteredMetaString = metaString
? this.applyFilters(metaString, contextSegments, configSnapshot)
: undefined;
if (metaPayload) {
log.meta = metaPayload;
}
if (error || message instanceof Error) {
const stack = style
? Logging.theme(
(error?.stack || (message as Error).stack) as string,
"stack",
level
)
: error?.stack || "";
const stackLabel =
typeof message === "string"
? filteredMessage
: (error || (message as Error)).message;
log.stack = ` | ${stackLabel} - Stack trace:\n${stack}`;
}
switch (this.config("format")) {
case "json":
return JSON.stringify(log);
case "raw": {
const generated = (this.config("pattern") as string)
.split(" ")
.map((s) => {
if (!s.match(/\{.*?}/g)) return s;
const formattedS = sf(s, log);
if (formattedS !== s) return formattedS;
return undefined;
})
.filter((s) => s)
.join(" ");
return filteredMetaString
? `${generated} ${filteredMetaString}`
: generated;
}
default:
throw new Error(`Unsupported logging format: ${this.config("format")}`);
}
}
private formatMeta(meta: LogMeta): string {
try {
return JSON.stringify(meta);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err: unknown) {
return String(meta);
}
}
/**
* @description Logs a message with the specified log level.
* @summary Checks if the message should be logged based on the current log level, then uses the appropriate console method to output the formatted log.
* @param {LogLevel} level - The log level of the message.
* @param {StringLike | Error} msg - The message to be logged or an Error object.
* @param {Error} [error] - Optional stack trace to include in the log.
* @return {void}
*/
protected log(
level: LogLevel,
msg: StringLike | Error,
error?: Error,
meta?: LogMeta
): void {
const confLvl = this.config("level") as LogLevel;
if (NumericLogLevels[confLvl] < NumericLogLevels[level]) return;
let method;
switch (level) {
case LogLevel.benchmark:
method = console.log;
break;
case LogLevel.info:
case LogLevel.verbose:
method = console.log;
break;
case LogLevel.debug:
method = console.debug;
break;
case LogLevel.error:
method = console.error;
break;
case LogLevel.trace:
method = console.trace;
break;
case LogLevel.warn:
method = console.warn;
break;
case LogLevel.silly:
method = console.debug;
break;
default:
throw new Error("Invalid log level");
}
method(this.createLog(level, msg, error, meta));
}
/**
* @description Logs a message at the benchmark level.
* @summary Logs a message at the benchmark level if the current verbosity setting allows it.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
benchmark(msg: StringLike, meta?: LogMeta): void {
this.log(LogLevel.benchmark, msg, undefined, meta);
}
/**
* @description Logs a message at the silly level.
* @summary Logs a message at the silly level if the current verbosity setting allows it.
* @param {StringLike} msg - The message to be logged.
* @param {number} [verbosity=0] - The verbosity level of the message.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
silly(
msg: StringLike,
verbosityOrMeta: number | LogMeta = 0,
meta?: LogMeta
): void {
const verbosity = typeof verbosityOrMeta === "number" ? verbosityOrMeta : 0;
const payloadMeta =
typeof verbosityOrMeta === "number" ? meta : verbosityOrMeta;
if ((this.config("verbose") as number) >= verbosity)
this.log(LogLevel.silly, msg, undefined, payloadMeta);
}
/**
* @description Logs a message at the verbose level.
* @summary Logs a message at the verbose level if the current verbosity setting allows it.
* @param {StringLike} msg - The message to be logged.
* @param {number} [verbosity=0] - The verbosity level of the message.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
verbose(
msg: StringLike,
verbosityOrMeta: number | LogMeta = 0,
meta?: LogMeta
): void {
const verbosity = typeof verbosityOrMeta === "number" ? verbosityOrMeta : 0;
const payloadMeta =
typeof verbosityOrMeta === "number" ? meta : verbosityOrMeta;
if ((this.config("verbose") as number) >= verbosity)
this.log(LogLevel.verbose, msg, undefined, payloadMeta);
}
/**
* @description Logs a message at the info level.
* @summary Logs a message at the info level for general application information.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
info(msg: StringLike, meta?: LogMeta): void {
this.log(LogLevel.info, msg, undefined, meta);
}
/**
* @description Logs a message at the debug level.
* @summary Logs a message at the debug level for detailed troubleshooting information.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
debug(msg: StringLike, meta?: LogMeta): void {
this.log(LogLevel.debug, msg, undefined, meta);
}
/**
* @description Logs a message at the error level.
* @summary Logs a message at the error level for errors and exceptions.
* @param {StringLike | Error} msg - The message to be logged or an Error object.
* @param {Error|object} [e] - Optional error or metadata to include in the log.
* @param {object} [meta] - Optional metadata to include with the entry when an error is supplied.
* @return {void}
*/
error(msg: StringLike | Error, e?: Error | LogMeta, meta?: LogMeta): void {
let errorCandidate: Error | undefined;
let payloadMeta: LogMeta | undefined;
if (e instanceof Error) {
errorCandidate = e;
payloadMeta = meta;
} else {
payloadMeta = e;
}
this.log(LogLevel.error, msg, errorCandidate, payloadMeta);
}
/**
* @description Logs a message at the warning level.
* @summary Logs a message at the warning level for potential issues.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
warn(msg: StringLike, meta?: LogMeta): void {
this.log(LogLevel.warn, msg, undefined, meta);
}
/**
* @description Logs a message at the trace level.
* @summary Logs a message at the trace level for tracing code execution.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
trace(msg: StringLike, meta?: LogMeta): void {
this.log(LogLevel.trace, msg, undefined, meta);
}
/**
* @description Updates the logger configuration.
* @summary Merges the provided configuration with the existing configuration.
* @param {Partial<LoggingConfig>} config - The configuration options to apply.
* @return {void}
*/
setConfig(config: Partial<LoggingConfig>): void {
this.conf = { ...(this.conf || {}), ...config };
}
get root(): string[] {
return [...this.baseContext];
}
/**
* @description Clears any contextual overrides applied by `for`.
* @summary Returns the same logger instance so more contexts can be chained afterwards.
* @return {this} The same logger instance.
*/
clear(): this {
this.context = [...this.baseContext];
return this;
}
}
/**
* @description A static class for managing logging operations.
* @summary The Logging class provides a centralized logging mechanism with support for different log levels, verbosity, and styling. It uses a singleton pattern to maintain a global logger instance and allows creating specific loggers for different classes and methods.
* @class Logging
* @example
* // Set global configuration
* Logging.setConfig({ level: LogLevel.debug, style: true });
*
* // Get a logger for a specific class
* const logger = Logging.for('MyClass');
*
* // Log messages at different levels
* logger.info('Application started');
* logger.debug('Processing data...');
*
* // Log with context
* const methodLogger = Logging.for('MyClass.myMethod');
* methodLogger.verbose('Detailed operation information', 1);
*
* // Log errors
* try {
* // some operation
* } catch (error) {
* logger.error(error);
* }
* @mermaid
* classDiagram
* class Logger {
* <<interface>>
* +for(method, config, ...args)
* +silly(msg, verbosity)
* +verbose(msg, verbosity)
* +info(msg)
* +debug(msg)
* +error(msg)
* +setConfig(config)
* }
*
* class Logging {
* -global: Logger
* -_factory: LoggerFactory
* -_config: LoggingConfig
* +setFactory(factory)
* +setConfig(config)
* +getConfig()
* +get()
* +verbose(msg, verbosity)
* +info(msg)
* +debug(msg)
* +silly(msg)
* +error(msg)
* +for(object, config, ...args)
* +because(reason, id)
* +theme(text, type, loggerLevel, template)
* }
*
* class MiniLogger {
* +constructor(context, conf?)
* }
*
* Logging ..> Logger : creates
* Logging ..> MiniLogger : creates by default
*/
export class Logging {
/**
* @description The global logger instance.
* @summary A singleton instance of Logger used for global logging.
*/
private static global?: Logger;
/**
* @description Factory function for creating logger instances.
* @summary A function that creates new Logger instances. By default, it creates a MiniLogger.
*/
private static _factory: LoggerFactory = (
object?: string,
config?: Partial<LoggingConfig>
) => {
const base =
typeof LoggedEnvironment.app === "string"
? [LoggedEnvironment.app as string]
: [];
return new MiniLogger(object, config, base) as unknown as Logger;
};
private static _config: typeof LoggedEnvironment = LoggedEnvironment;
private constructor() {}
/**
* @description Sets the factory function for creating logger instances.
* @summary Allows customizing how logger instances are created.
* @param {LoggerFactory} factory - The factory function to use for creating loggers.
* @return {void}
*/
static setFactory(factory: LoggerFactory) {
Logging._factory = factory;
this.global = undefined;
}
/**
* @description Updates the global logging configuration.
* @summary Allows updating the global logging configuration with new settings.
* @param {Partial<LoggingConfig>} config - The configuration options to apply.
* @return {void}
*/
static setConfig(config: Partial<LoggingConfig>): void {
Object.entries(config).forEach(([k, v]) => {
(this._config as any)[k] = v as any;
});
}
/**
* @description Gets a copy of the current global logging configuration.
* @summary Returns a copy of the current global logging configuration.
* @return {LoggingConfig} A copy of the current configuration.
*/
static getConfig(): typeof LoggedEnvironment {
return this._config;
}
/**
* @description Retrieves or creates the global logger instance.
* @summary Returns the existing global logger or creates a new one if it doesn't exist.
* @return {Logger} The global Logger instance.
*/
static get(): Logger {
return this.ensureRoot();
}
/**
* @description Logs a verbose message.
* @summary Delegates the verbose logging to the global logger instance.
* @param {StringLike} msg - The message to be logged.
* @param {number|object} [verbosity] - The verbosity level or metadata object.
* @param {object} [meta] - Optional metadata applied when a verbosity level is provided.
* @return {void}
*/
static verbose(
msg: StringLike,
verbosityOrMeta: number | LogMeta = 0,
meta?: LogMeta
): void {
return this.get().verbose(msg, verbosityOrMeta, meta);
}
/**
* @description Logs an info message.
* @summary Delegates the info logging to the global logger instance.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
static info(msg: StringLike, meta?: LogMeta): void {
return this.get().info(msg, meta);
}
/**
* @description Logs a trace message.
* @summary Delegates the trace logging to the global logger instance.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
static trace(msg: StringLike, meta?: LogMeta): void {
return this.get().trace(msg, meta);
}
/**
* @description Logs a debug message.
* @summary Delegates the debug logging to the global logger instance.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
static debug(msg: StringLike, meta?: LogMeta): void {
return this.get().debug(msg, meta);
}
/**
* @description Logs a benchmark message.
* @summary Delegates the benchmark logging to the global logger instance.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
static benchmark(msg: StringLike, meta?: LogMeta): void {
return this.get().benchmark(msg, meta);
}
/**
* @description Logs a silly message.
* @summary Delegates the silly logging to the global logger instance.
* @param {StringLike} msg - The message to be logged.
* @param {number|object} [verbosity] - The verbosity level or metadata object.
* @param {object} [meta] - Optional metadata applied when a verbosity level is provided.
* @return {void}
*/
static silly(
msg: StringLike,
verbosityOrMeta: number | LogMeta = 0,
meta?: LogMeta
): void {
return this.get().silly(msg, verbosityOrMeta, meta);
}
/**
* @description Logs a warning message.
* @summary Delegates the warning logging to the global logger instance.
* @param {StringLike} msg - The message to be logged.
* @param {object} [meta] - Optional metadata to include with the entry.
* @return {void}
*/
static warn(msg: StringLike, meta?: LogMeta): void {
return this.get().warn(msg, meta);
}
/**
* @description Logs an error message.
* @summary Delegates the error logging to the global logger instance.
* @param {StringLike | Error} msg - The message to be logged.
* @param {Error|object} [e] - Optional error or metadata to include in the log.
* @param {object} [meta] - Optional metadata to include with the entry when an error is supplied.
* @return {void}
*/
static error(
msg: StringLike | Error,
e?: Error | LogMeta,
meta?: LogMeta
): void {
return this.get().error(msg, e, meta);
}
/**
* @description Creates a logger for a specific object or context.
* @summary Creates a new logger instance for the given object or context using the factory function.
* @param {LoggingContext} object - The object, class, or context to create a logger for.
* @param {Partial<LoggingConfig>} [config] - Optional configuration to override global settings.
* @param {...any} args - Additional arguments to pass to the logger factory.
* @return {Logger} A new logger instance for the specified object or context.
*/
static for(
object: LoggingContext,
config?: Partial<LoggingConfig>,
...args: any[]
): Logger {
const root = this.global ? this.global : this.ensureRoot(args);
const callArgs = config !== undefined ? [object, config] : [object];
return (root.for as any)(...callArgs);
}
/**
* @description Creates a logger for a specific reason or correlation context.
* @summary Utility to quickly create a logger labeled with a free-form reason and optional identifier so that ad-hoc operations can be traced without tying the logger to a class or method name.
* @param {string} reason - A textual reason or context label for this logger instance.
* @param {string} [id] - Optional identifier to help correlate related log entries.
* @return {Logger} A new logger instance labeled with the provided reason and id.
*/
static because(reason: string, id?: string): Logger {
const root = this.ensureRoot();
let logger = (root.for as any)(reason, this._config);
if (id) logger = (logger.for as any)(id);
return logger;
}
private static baseContext(): string[] {
const app = this._config.app;
return typeof app === "string" && app.length ? [app] : [];
}
private static attachRootContext(logger: Logger): Logger {
const base =
(logger as any).root && Array.isArray((logger as any).root)
? [...(logger as any).root]
: this.baseContext();
if (
!(logger as any).context ||
(Array.isArray((logger as any).context) &&
(logger as any).context.length === 0)
) {
(logger as any).context = [...base];
}
(logger as any)[ROOT_CONTEXT_SYMBOL] = [...base];
return logger;
}
private static ensureRoot(extras: any[] = []): Logger {
if (!this.global) {
const instance = this._factory(undefined, undefined, ...extras);
this.global = this.attachRootContext(instance);
}
return this.global;
}
/**
* @description Applies theme styling to text.
* @summary Applies styling (colors, formatting) to text based on the theme configuration.
* @param {string} text - The text to style.
* @param type - The type of element to style (e.g., "class", "message", "logLevel").
* @param {LogLevel} loggerLevel - The log level to use for styling.
* @param {Theme} [template=DefaultTheme] - The theme to use for styling.
* @return {string} The styled text.
* @mermaid
* sequenceDiagram
* participant Caller
* participant Theme as Logging.theme
* participant Apply as apply function
* participant Style as styled-string-builder
*
* Caller->>Theme: theme(text, type, loggerLevel)
* Theme->>Theme: Check if styling is enabled
* alt styling disabled
* Theme-->>Caller: return original text
* else styling enabled
* Theme->>Theme: Get theme for type
* alt theme not found
* Theme-->>Caller: return original text
* else theme found
* Theme->>Theme: Determine actual theme based on log level
* Theme->>Apply: Apply each style property
* Apply->>Style: Apply colors and formatting
* Style-->>Apply: Return styled text
* Apply-->>Theme: Return styled text
* Theme-->>Caller: Return final styled text
* end
* end
*/
static theme(
text: string,
type: keyof Theme | keyof LogLevel,
loggerLevel: LogLevel,
template: Theme = DefaultTheme
) {
if (!this._config.style) return text;
function apply(
txt: string,
option: keyof ThemeOption,
value: number | [number] | [number, number, number] | number[] | string[]
): string {
try {
const t: string | StyledString = txt;
let c = style(t);
function applyColor(
val: number | [number] | [number, number, number],
isBg = false
): StyledString {
let f:
| typeof c.background
| typeof c.foreground
| typeof c.rgb
| typeof c.color256 = isBg ? c.background : c.foreground;
if (!Array.isArray(val)) {
return (f as typeof c.background | typeof c.foreground).call(
c,
value as number
);
}
switch (val.length) {
case 1:
f = isBg ? c.bgColor256 : c.color256;
return (f as typeof c.bgColor256 | typeof c.color256)(val[0]);
case 3:
f = isBg ? c.bgRgb : c.rgb;
return c.rgb(val[0], val[1], val[2]);
default:
console.error(`Not a valid color option: ${option}`);
return style(t as string);
}
}
function applyStyle(v: number | string): void {
if (typeof v === "number") {
c = c.style(v);
} else {
c = c[v as keyof ColorizeOptions] as StyledString;
}
}
switch (option) {
case "bg":
case "fg":
return applyColor(value as number).text;
case "style":
if (Array.isArray(value)) {
value.forEach(applyStyle);
} else {
applyStyle(value as number | string);
}
return c.text;
default:
console.error(`Not a valid theme option: ${option}`);
return t;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: unknown) {
console.error(`Error applying style: ${option} with value ${value}`);
return txt;
}
}
const individualTheme = template[type as keyof Theme];
if (!individualTheme || !Object.keys(individualTheme).length) {
return text;
}
let actualTheme: ThemeOption = individualTheme as ThemeOption;
const logLevels = Object.assign({}, LogLevel);
if (Object.keys(individualTheme)[0] in logLevels)
actualTheme =
(individualTheme as ThemeOptionByLogLevel)[loggerLevel] || {};
return Object.keys(actualTheme).reduce((acc: string, key: string) => {
const val = (actualTheme as ThemeOption)[key as keyof ThemeOption];
if (val)
return apply(
acc,
key as keyof ThemeOption,
val as
| number
| [number]
| [number, number, number]
| number[]
| string[]
);
return acc;
}, text);
}
}
Source