UNPKG

symfony-style-console

Version:

Use the style and utilities of the Symfony Console in Node.js

240 lines (239 loc) 7.19 kB
import OutputFormatterStyle from './OutputFormatterStyle'; import OutputFormatterStyleStack from './OutputFormatterStyleStack'; import { trimEnd, safeReplace } from '../Helper/Helper'; /** * Formatter class for console output. * * * @author Konstantin Kudryashov <ever.zet@gmail.com> * * Original PHP class * * @author Florian Reuschel <florian@loilo.de> * * Port to TypeScript */ export default class OutputFormatter { /** * Initializes console output formatter. * * @param decorated Whether this formatter should actually decorate strings * @param styles Object mappping names to [[OutputFormatterStyleInterface]] instances */ constructor(decorated = false, styles = {}) { /** * A mapping of formatter style names to their respective classes. */ this.styles = {}; this.decorated = !!decorated; this.setStyle('error', new OutputFormatterStyle('white', 'red')); this.setStyle('info', new OutputFormatterStyle('green')); this.setStyle('comment', new OutputFormatterStyle('yellow')); this.setStyle('question', new OutputFormatterStyle('black', 'cyan')); for (const name in styles) { this.setStyle(name, styles[name]); } this.styleStack = new OutputFormatterStyleStack(); } /** * Escapes the "<" special char in given text. * * @param text Text to escape * @return Escaped text */ static escape(text) { text = text.replace(/([^\\]?)</g, '$1\\<'); return OutputFormatter.escapeTrailingBackslash(text); } /** * Escapes trailing backslash "\" in given text. * * @param text Text to escape * @return Escaped text * * @internal */ static escapeTrailingBackslash(text) { if ('\\' === text.slice(-1)) { const len = text.length; text = trimEnd(text, '\\'); text += '<<'.repeat(len - text.length); } return text; } /** * Creates a new instance containing the same `decorated` flag and styles. * * @return The cloned [[OutputFormatter]] instance */ clone() { return new OutputFormatter(this.isDecorated(), Object.assign({}, this.styles)); } /** * Sets the `decorated` flag property. * * @param decorated Whether to decorate the messages or not */ setDecorated(decorated) { this.decorated = !!decorated; } /** * Gets the `decorated` flag property. * * @return True if the output will decorate messages, false otherwise */ isDecorated() { return this.decorated; } /** * Sets a new style. * * @param name The style name * @param style The style instance */ setStyle(name, style) { this.styles[name.toLowerCase()] = style; } /** * Checks if output formatter has style with specified name. * * @param string name * @return bool */ hasStyle(name) { return !!this.styles[name.toLowerCase()]; } /** * Gets style options from style with specified name. * * @param name * @return The style * * @throws ReferenceError When style isn't defined */ getStyle(name) { if (!this.hasStyle(name)) { throw new ReferenceError(`Undefined style: ${name}`); } return this.styles[name.toLowerCase()]; } /** * Formats a message according to the given styles. * * @param message The message to style * * @return The styled message */ format(message) { message = String(message); let offset = 0; let output = ''; const tagRegex = '[a-z][a-z0-9,_=;-]*'; const tagsRegex = new RegExp(`<((${tagRegex})|\\/(${tagRegex})?)>`, 'gi'); let match; while ((match = tagsRegex.exec(message))) { const text = match[0]; const pos = match.index; if (0 != pos && '\\' == message[pos - 1]) { continue; } // add the text up to the next tag output += this.applyCurrentStyle(message.substr(offset, pos - offset)); offset = pos + text.length; // opening tag? const open = '/' !== text[1]; let tag; if (open) { tag = match[1]; } else { tag = match[3] || ''; } const style = this.createStyleFromString(tag.toLowerCase()); if (!open && !tag) { // </> this.styleStack.pop(); } else if (false === style) { output += this.applyCurrentStyle(text); } else if (open) { this.styleStack.push(style); } else { this.styleStack.pop(style); } } output += this.applyCurrentStyle(message.slice(offset)); if (-1 !== output.indexOf('<<')) { return safeReplace(output, { '\\<': '<', '<<': '\\' }); } return output.replace(/\\</g, '<'); } /** * Gets the used style stack. */ getStyleStack() { return this.styleStack; } /** * Tries to create new [[OutputFormatterStyleInterface]] instance from string. * * @param str The string to create the formatter style from * @return `false` if `str` is not format string */ createStyleFromString(str) { if (this.styles[str]) { return this.styles[str]; } const styleRegex = /([^=]+)=([^;]+)(;|$)/g; const style = new OutputFormatterStyle(); let gotMatch = false; let match; while ((match = styleRegex.exec(str))) { gotMatch = true; if ('fg' == match[1]) { style.setForeground(match[2]); } else if ('bg' == match[1]) { style.setBackground(match[2]); } else if ('options' === match[1]) { const options = Array.from(match[2].match(/([^,;]+)/g)); for (const option of options) { try { style.setOption(option); } catch (e) { console.warn(`Unknown style options are deprecated since version 3.2 \ and will be removed in 4.0. Error "${e}".`); return false; } } } else { return false; } } if (gotMatch) { return style; } else { return false; } } /** * Applies current style from stack to text, if must be applied. * * @param text The text to format * @return The formatted text */ applyCurrentStyle(text) { return this.isDecorated() && text.length > 0 ? this.styleStack.getCurrent().apply(text) : text; } }