UNPKG

@maddimathon/utility-typescript

Version:

TypeScript utilities (types, functions, classes) to use in various projects.

523 lines 20.4 kB
/** * @since 0.1.1 * * @packageDocumentation */ /*! * @maddimathon/utility-typescript@2.0.0-beta * @license MIT */ import { AbstractConfigurableClass } from './abstracts/AbstractConfigurableClass.js'; import { mergeArgs, softWrapText, timestamp, typeOf, } from '../functions/index.js'; /** * A configurable class for formatting message strings for various outputs. * * @since 0.1.1 — Experimental * * @experimental */ export class MessageMaker extends AbstractConfigurableClass { /* STATIC * ====================================================================== */ /** * Returns the default painter callback function for the given format. * * `'html'` and `'markdown'` default painters currently do not apply any * colours. * * Used only by {@link MessageMaker.buildArgs}. * * @category Static * * @param classArgs A complete arguments object. Requires complete to * avoid building complete arguments multiple times. */ static defaultPainter(classArgs) { // returns on match switch (classArgs.paintFormat) { case 'html': return (line, args = {}) => { // returns if (!args.bold && !args.clr && !args.flag && !args.italic) { return line; } if (args.italic) { line = '<em>' + line + '</em>'; } if (args.bold) { line = '<strong>' + line + '</strong>'; } return line; }; case 'markdown': return (line, args = {}) => { // returns if (!args.bold && !args.clr && !args.flag && !args.italic) { return line; } if (args.italic) { line = '_' + line + '_'; } if (args.bold) { line = '**' + line + '**'; } return line; }; case 'node': return (line, args = {}) => { // returns // if no styling is to be applied, then we can return early if (!args.bold && !args.clr && !args.flag && !args.italic) { return line; } /** The complete ansi escape string, joined by `;`. */ const ansi = []; // paint colour if (args.clr || args.flag) { /** Colour depth available in this console. */ const clrDepth = typeof process.stdout.getColorDepth === 'function' ? process.stdout.getColorDepth() : 1; // start by assuming it's the foreground /** Background colour slug for this text. */ let bg = null; /** Foreground colour slug for this text. */ let fg = args.clr ?? 'black'; if (args.flag) { if (args.flag == 'reverse' && clrDepth > 4) { switch (fg) { case 'grey': bg = 'light-grey'; fg = 'black'; break; default: bg = 'light-grey'; break; } if (clrDepth === 8) { switch (fg) { case 'orange': case 'pink': bg = 'grey'; break; } } } else { bg = args.clr ?? 'black'; fg = clrDepth > 4 ? 'white' : null; } } switch (clrDepth) { case 4: // setting the foreground to black (without a // background) can make text invisible in dark // modes if (bg || fg !== 'black') { ansi.push([ fg ? classArgs.ansiColours[4].fg[fg] : '', bg ? classArgs.ansiColours[4].bg[bg] : '', ].filter(s => s).join(';')); } break; case 8: case 24: if (bg && args.flag !== 'reverse') { if (clrDepth == 8) { switch (bg) { case 'light-grey': case 'orange': case 'yellow': fg = 'black'; break; default: fg = 'white'; break; } } else { switch (bg) { case 'grey': case 'yellow': fg = 'black'; break; default: fg = 'white'; break; } } } // setting the foreground to black (without a // background) can make text invisible in dark // modes if (bg || fg !== 'black') { if (fg) { ansi.push(`38;${classArgs.ansiColours[clrDepth][fg]}`); } if (bg) { ansi.push(`48;${classArgs.ansiColours[clrDepth][bg]}`); } } break; } } // paint style if (args.bold) { ansi.push('1'); } if (args.italic) { ansi.push('3'); } return '\x1b[' + ansi.join(';') + 'm' + line + '\x1b[0m'; }; } return null; } /* LOCAL PROPERTIES * ====================================================================== */ get ARGS_DEFAULT() { return { ansiColours: { 4: { fg: { black: '30', grey: '30', 'light-grey': '37', white: '37', red: '31', orange: '33', yellow: '33', green: '32', turquoise: '36', blue: '34', purple: '35', pink: '35', }, bg: { black: '40', grey: '40', 'light-grey': '47', white: '47', red: '41', orange: '43', yellow: '43', green: '42', turquoise: '46', blue: '44', purple: '45', pink: '45', }, }, 8: { black: '5;232', grey: '5;241', 'light-grey': '5;247', white: '5;255', red: '5;124', orange: '5;166', yellow: '5;208', green: '5;28', turquoise: '5;30', blue: '5;20', purple: '5;55', pink: '5;162', }, 24: { black: '2;26;26;26', grey: '2;108;108;108', 'light-grey': '2;208;208;208', white: '2;248;248;248', red: '2;168;36;36', orange: '2;174;84;4', yellow: '2;204;182;0', green: '2;24;118;10', turquoise: '2;0;128;98', blue: '2;60;84;157', purple: '2;129;75;155', pink: '2;179;77;145', }, }, argsRecursive: true, msg: { bold: false, clr: null, depth: 0, flag: false, fullWidth: false, hangingIndent: '', indent: '', italic: false, linesIn: 0, linesOut: 1, minWidth: 20, maxWidth: null, tab: ' ', }, painter: null, paintFormat: null, paintIfEmpty: false, }; } /** * Build a complete args object. * * @category Args */ buildArgs(args) { const mergedDefault = this.ARGS_DEFAULT; // using this.mergeArgs here can cause issues because // this method is sometimes called from the prototype const built = mergeArgs(mergedDefault, args, this.ARGS_DEFAULT.argsRecursive); if (args?.msg && typeof args.msg !== 'function') { // be should merge these defaults better if (typeof mergedDefault.msg !== 'function') { built.msg = mergeArgs(mergedDefault.msg, args.msg, true); } if (args.msg.minWidth) { // we know this type will match the args type, even if we change // the type assigned to args built.msg.minWidth = Math.max(10, args.msg.minWidth); } } // We should add a default painter if (!built.painter && built.paintFormat) { built.painter = MessageMaker.defaultPainter(built); } return built; } /** * Build a complete args object. * * @category Args */ msgArgs(args) { const merged = mergeArgs(this.args.msg, args, false); if (merged.maxWidth !== null) { const indentWidth = merged.maxWidth - ((args?.tab?.length ?? 0) * (args?.depth ?? 0)); merged.maxWidth = Math.max(10, merged.minWidth, indentWidth); if (merged.flag) { merged.maxWidth = merged.maxWidth - 2; } } return merged; } /* CONSTRUCTOR * ====================================================================== */ constructor(args = {}) { super(args); } /* METHODS * ====================================================================== */ /** * Joins string arrays with a single new line and adds an indent to the * beginning of every line, and adds next level of indent for child arrays. * * @category Formatters * * @param lines String to implode. Arrays are joined with `'\n'`. * @param indent Optional. Default `this.args.msg.tab`. * * @return The same text, but with an indent added after every new line. */ implodeWithIndent(lines, indent = this.args.msg.tab) { return lines.map((line) => { switch (typeOf(line)) { case 'array': return this.implodeWithIndent(line, indent + this.args.msg.tab); case 'string': return indent + line; default: return indent + String(line); } }).flat().join('\n'); } /** * Used to map each line of a message in {@link MessageMaker.msg}. * * Does not wrap or split it (assumes this has already been done). Applies * {@link MessageMaker.painter} and {@link MessageMaker.Args.depth} indent. * * @category Messagers * * @param line String to map. Already wrapped to line width, if applicable. * @param args Message arguments that apply to this line. Also passed to {@link MessageMaker.painter}. * @param prefix Optional. Unpainted string added before the line. Helpful for hanging indents. Default ''. */ lineMapper(line, args, prefix = '') { if (args.maxWidth && args.fullWidth) { if (line.length < args.maxWidth) { line = line + ' '.repeat(args.maxWidth - line.length); } } if (args.flag && this.args.paintFormat === 'node' && line.match(/^[\s\n]*$/gi) === null) { line = ' ' + line + ' '; } return args.tab.repeat(args.depth) + args.indent + prefix + this.painter(line, args); } /** * Formats the given message according to options. * * @category Messagers * * @param msg Message to display. If it's an array, the strings are joined with `'\n'`. * @param _args Optional. Overrides for default arguments in {@link MessageMaker.args}. */ msg(msg, _args = {}) { // VariableInspector.dump( { 'MessageMaker.msg() _args': _args } ); const args = this.msgArgs(_args); // if it's an array, we want to join them and then split them again if (Array.isArray(msg)) { msg = msg.join('\n'); } if (args.maxWidth) { msg = softWrapText(msg, args.maxWidth); } const lines = msg.split(/\n/g); return ('\n'.repeat(args.linesIn ?? 0) + lines.map((line, index) => { return this.lineMapper(line, args, index > 0 ? args.hangingIndent : ''); }).join('\n') + '\n'.repeat(args.linesOut ?? 0)); } /** * Formats given messages individually and then joins them on return. * * @category Messagers * * @param messages Messages to display, each with their own personal override arguments. Joined with `universalArgs.joiner` (default `'\n\n'`) before return. * @param universalArgs Optional. Overrides for default arguments in {@link MessageMaker.args} for all messages. */ msgs(messages, universalArgs = {}) { // VariableInspector.dump( { 'MessageMaker.msgs() _auniversalArgsrgs': universalArgs } ); if (!Array.isArray(messages)) { messages = [messages]; } // We will add the lines a better way. const defaultUniversalArgs = { ...universalArgs, linesIn: 0, linesOut: 0, }; const ret = []; messages.forEach(([_msg, _args], index) => { _args = this.mergeArgs(defaultUniversalArgs, _args ?? {}, true); if (index > 0 && universalArgs.hangingIndent && defaultUniversalArgs.joiner?.match(/\n/g)) { _args = { ..._args, hangingIndent: '', indent: universalArgs.hangingIndent ?? this.ARGS_DEFAULT.msg.hangingIndent, }; } ret.push(this.msg(_msg, _args)); }); return ('\n'.repeat(universalArgs.linesIn ?? 0) + ret.join(universalArgs.joiner ?? '\n\n') + '\n'.repeat(universalArgs.linesOut ?? 0)); } /** * Applies colour and font styles to an message for output. * * @category Stylers */ painter(msg, args = {}) { // returns if empty if (!this.args.paintIfEmpty) { // returns if (!msg.replace(/[\n\s\r]+/gs, '').length) { return msg; } } if (this.args.painter) { msg = this.args.painter(msg, args); } return msg; } /** * Formats the given message according to options. * * @category Messagers * * @param msg Message to display. If it's an array, the strings are joined with `'\n'`. * @param msgArgs Optional. Overrides for default arguments in {@link MessageMaker['msgArgs']}. Used for the whole message. * @param timeArgs Optional. Overrides for default arguments in {@link MessageMaker['msgArgs']}. Used only for the timestamp. */ timestampMsg(msg, msgArgs = {}, timeArgs = {}) { /** A complete version of the base message arguments. */ const args_full = this.msgArgs({ joiner: '\n\n', ...msgArgs, }); // we want to accept a variety of inputs, but we need to normalize it to // be MessageMaker.BulkMsgs if (typeof msg === 'string') { msg = msg ? [[msg]] : []; args_full.joiner = args_full.joiner ?? '\n'; } else { // = is an array msg = msg.map((m) => { if (typeof m === 'string') { args_full.joiner = args_full.joiner ?? '\n'; m = [m]; } const m_arr = m; return m_arr; }); } // the actual values to be used for the whole message, but ignore when // formatting the message parts const { depth, linesIn, linesOut, } = args_full; /** This is the unpainted string used for the timestamp. */ const timePrefix = `[${timestamp(timeArgs.date ?? null, { date: false, time: true, ...timeArgs.stamp, })}]`; /** Base arguments to use for each individual message part. */ const args_parts = { ...args_full, depth: 0, linesIn: 0, linesOut: 0, hangingIndent: (args_full.hangingIndent + ' '.repeat(timePrefix.length + 1) + args_full.tab.repeat(depth)), }; /** Properly formatted as bulk messages. */ const messages = msg; /** Compiled strings for each message part. */ const compiledMessages = { message: (messages.length ? ((args_parts.flag ? this.msg(' ', args_parts) : ' ') + this.msgs(messages, args_parts)) : ''), timestamp: this.msg(timePrefix, this.mergeArgs(args_parts, { flag: false, italic: false, ...timeArgs, depth, linesIn: 0, linesOut: 0, }, false)), }; return ('\n'.repeat(linesIn ?? 0) + compiledMessages.timestamp + compiledMessages.message + '\n'.repeat(linesOut ?? 0)); } } /** * Used only for {@link MessageMaker}. * * @since 0.1.1 */ (function (MessageMaker) { ; ; ; ; ; })(MessageMaker || (MessageMaker = {})); //# sourceMappingURL=MessageMaker.js.map