@zowe/imperative
Version:
framework for building configurable CLIs
333 lines • 15.5 kB
JavaScript
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TextUtils = void 0;
const util_1 = require("util");
/**
* Lightweight utilities for text manipulation/coloring.
* Low import impact
*/
class TextUtils {
/**
* Get the recommended width to wrap text. You can specify a preferred width,
* but this method width return
* @param {number} preferredWidth - the width you would like to use if supported
* by the user's terminal
* @returns {number} - the width that will work best for the user's terminal
*/
static getRecommendedWidth(preferredWidth) {
var _a;
if (preferredWidth === void 0) { preferredWidth = ((_a = process === null || process === void 0 ? void 0 : process.stdout) === null || _a === void 0 ? void 0 : _a.columns) ? process.stdout.columns : TextUtils.DEFAULT_WRAP_WIDTH; }
const widthSafeGuard = 8; // prevent partial words from continuing over lines
const yargs = require("yargs");
// eslint-disable-next-line no-extra-parens
const maxWidth = (yargs.terminalWidth() != null && yargs.terminalWidth() > 0) ?
yargs.terminalWidth() - widthSafeGuard : preferredWidth;
return Math.min(preferredWidth, maxWidth);
}
static renderWithMustache(template, values) {
const mustache = require("mustache");
mustache.escape = (value) => {
// don't do HTML escaping
return value;
};
return mustache.render(template, values);
}
/**
* Replace keys from an object with string explanations for those keys,
* primarily so that they can be printed for the user to read.
* @param original - the original object e.g. a response from a z/OSMF API {wrdKy4U: "weirdkeyvalue"}
* @param explanationMap - an object that maps the original to the new format
* @param includeUnexplainedKeys - should keys not covered by
* the explanation object be included in the result?
* @returns {any} - the explained object
*/
static explainObject(original, explanationMap, includeUnexplainedKeys = true) {
// no object to explain, return null
if (original == null) {
return null;
}
// no explanation map, return original
if (explanationMap == null) {
return original;
}
// if original is array, then iterate through it recursively and return explained array
if (Array.isArray(original)) {
const explainedArray = [];
for (const item of original) {
explainedArray.push(TextUtils.explainObject(item, explanationMap, includeUnexplainedKeys));
}
if (explainedArray.length === 0) {
return "none";
}
return explainedArray;
}
// future explained object
const explainedObject = {};
// array of keys to be ignored
const ignoredKeys = explanationMap.ignoredKeys ? explanationMap.ignoredKeys.split(",") : [];
// iterate through all keys in original object
for (const key of Object.keys(original)) {
let isKeyIncluded = true;
// key in ignored list. Skip it
if (ignoredKeys.indexOf(key) !== -1) {
isKeyIncluded = false;
}
// key found, let's translate it
if (explanationMap[key] && isKeyIncluded) {
if (typeof explanationMap[key] === "string") {
// translate this key and assign the original value
explainedObject[explanationMap[key]] = original[key];
}
else {
// inner object found in the explanation map. We need to translate this one as well with recursive call
const childExplanationMap = explanationMap[key];
const explainedKey = childExplanationMap.explainedParentKey ? childExplanationMap.explainedParentKey : key;
explainedObject[explainedKey] = TextUtils.explainObject(original[key], childExplanationMap, includeUnexplainedKeys);
}
}
else if (includeUnexplainedKeys) {
explainedObject[key] = original[key];
}
}
return explainedObject;
}
/**
* Get a json object in tabular form
* @param {any} object: Any JSON object
* @param {any } options: Any JSON object to specify printing
* @param color use color on the result?
*/
static prettyJson(object, options, color = true, append = "\n\n") {
const prettyjson = require("prettyjson");
/**
* Default options for printing prettyJson
*/
const defaultOptions = !color || process.env.FORCE_COLOR === "0" ? {
noColor: true
} : {
keysColor: "yellow"
};
/**
* If user specifies prettyJson options use those instead of default
*/
return prettyjson.render(object, options || defaultOptions)
.replace(/""" *\n((.|\n)*?)"""/g, "$1") + append;
}
/**
*
* @param {any[]} objects - the key-value objects to convert to a
* @param primaryHighlightColor - the main color to highlight headings of the table with. e.g. "blue"
* @param {number} maxColumnWidth - override the default column width of the table?
* @param {boolean} includeHeader - should the table include a header of the field names of the objects
* @param includeBorders - should the table have borders between the cells?
* @param hardWrap - hard wrap the text within the width of the table cells (defaults to false)
* @param headers - specify which headers in which order to display. if omitted, loops through the rows
* and adds object properties as headers in their enumeration order
* @returns {string} the rendered table
*/
static getTable(objects, primaryHighlightColor, maxColumnWidth, includeHeader = true, includeBorders = false, hardWrap = false, headers) {
const Table = require("cli-table3");
// if the user did not specify which headers to use, build them from the object array
if (!headers) {
headers = this.buildHeaders(objects);
}
if (maxColumnWidth == null) {
maxColumnWidth = this.getRecommendedWidth() / headers.length;
}
const borderChars = includeBorders ?
{
"top": "═", "top-mid": "╤",
"top-left": "╔", "top-right": "╗",
"bottom": "═", "bottom-mid": "╧",
"bottom-left": "╚",
"bottom-right": "╝",
"left": "║",
"left-mid": "╟",
"right": "║",
"right-mid": "╢"
} : {
"top": "", "top-mid": "", "top-left": "", "top-right": "",
"bottom": "", "bottom-mid": "", "bottom-left": "", "bottom-right": "",
"left": "", "left-mid": "", "mid": "", "mid-mid": "",
"right": "", "right-mid": "", "middle": " "
};
const table = new Table({
// colWidths: headers.map((header) => {
// return header.length > maxColWidth ? maxColWidth + pad : header.length + pad;
// }),
// highlight the headers
head: includeHeader ? headers.map((header) => {
return TextUtils.wordWrap(TextUtils.chalk[primaryHighlightColor](header), maxColumnWidth, "", hardWrap);
}) : [],
chars: borderChars,
style: { "padding-left": 0, "padding-right": 0, "head": [], "border": includeBorders ? [] : undefined }
});
for (const obj of objects) {
const row = headers.map((header) => {
return TextUtils.wordWrap(obj[header] || "", maxColumnWidth, "", hardWrap);
});
table.push(row);
}
return table.toString();
}
/**
* Build table headers from an array of key-value objects
* @param {any[]} objects - the key-value objects from which to build headers
* @returns {string} the headers array
*/
static buildHeaders(objects) {
const headers = [];
// for every property of every object in the array,
// make a header for the table
for (const obj of objects) {
for (const key of Object.keys(obj)) {
if (headers.indexOf(key) === -1) {
headers.push(key);
}
}
}
return headers;
}
/**
* Wrap some text so that it fits within a certain width with the wrap-ansi package
* @param {string} text The text you would like to wrap
* @param {number} width - The width you would like to wrap to - we'll try to determine the
* optimal width based on this (the resulting wrap may be wrapped to fewer columns, but not more)
* @param {string} indent - Add this string to every line of the result
* @param {boolean} hardWrap - do not allow any letters past the requested width - defaults to false
* @returns {string}
*/
static wordWrap(text, width, indent = "", hardWrap = false, trim = true) {
const wrappedText = require("wrap-ansi")(text, this.getRecommendedWidth(width), { hard: hardWrap, trim });
return TextUtils.indentLines(wrappedText, indent);
}
/**
* Indent some text
* @param {string} text The text you would like to indent
* @param {string} indent - Add this string to every line of the result
* @returns {string}
*/
static indentLines(text, indent = "") {
return text.split(/\n/g).map((line) => {
if (line.length === 0) {
return line;
}
return indent + line;
}).join("\n");
}
/**
* Highlight all matches of a full regex with TextUtils.chalk
* @param {string} text - the text you'd like to search for matches
* @param {RegExp} term - a regular expression of terms to highlight
* @returns {string} - the highlighted string
*/
static highlightMatches(text, term) {
return text.replace(term, (match) => {
return TextUtils.chalk.blue(match);
});
}
/**
* Auto-detect whether a message should be formatted with printf-style formatting or mustache
* (but don't try to use both!) and format the string accordingly
* @param {string} message - the string message with %s or {{mustache}} style variables
* @param values the fields that will resolve the printf or mustache template
* @returns {string} - a formatted string with the variables inserted
*/
static formatMessage(message, ...values) {
if (!(values == null)) {
const isPrintfValue = (value) => {
let isJson = false;
try {
JSON.parse(value);
isJson = true;
}
catch (e) {
// not json
}
return typeof value === 'string' || typeof value === 'number' || isJson;
};
if (Array.isArray(values) && values.filter(isPrintfValue).length === values.length) {
message = util_1.format.apply(this, [message, ...values]);
}
else {
message = TextUtils.renderWithMustache.apply(this, [message, ...values]);
}
}
return message;
}
static get chalk() {
const mChalk = require("chalk");
// chalk is supposed to handle this, but I think it only does so the first time it is loaded
// so we need to check ourselves in case we've changed the environmental variables
if (process.env.MARKDOWN_GEN != null) {
mChalk.level = 0;
}
else if (process.env.FORCE_COLOR != null) {
const parsedInt = parseInt(process.env.FORCE_COLOR);
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
if (!isNaN(parsedInt) && parsedInt >= 0 && parsedInt <= 3) {
mChalk.level = parsedInt;
}
else if (process.env.FORCE_COLOR === "true") {
mChalk.level = 1;
}
}
return mChalk;
}
/**
* Parse a key value string into an object
* @param {string} keysAndValues - a string in the format key1=value1,key2=value2,key3=value3.
* Note: the key names are case sensitive
* @returns {{[key: string]: string}} the parsed object
*/
static parseKeyValueString(keysAndValues) {
const parsedObject = {};
const keyValueExample = "key1=value1,key2=value2,key3=value3,key4WithCommaAndEquals=my\\=key\\,is good";
// count unescaped commas and equals signs
const numberOfEqualsSigns = (keysAndValues.match(/[^\\]=/g) || []).length;
const numberOfCommas = (keysAndValues.match(/[^\\],/g) || []).length;
if (!/[^\\]=/g.test(keysAndValues) ||
numberOfEqualsSigns > 1 && numberOfCommas !== numberOfEqualsSigns - 1) {
throw new Error("The keys and values provided are not in the expected format. Example of expected format: " + keyValueExample);
}
// make it easier to deal with the key value string by replacing unescaped equals signs and commas
const keyValueEntrySplitKey = "_SPLIT_ENTRY_";
const valueSplitKey = "_SPLIT_KEY_";
keysAndValues = keysAndValues.replace(/([^\\]),/g, "$1" + keyValueEntrySplitKey);
keysAndValues = keysAndValues.replace(/([^\\])=/g, "$1" + valueSplitKey);
keysAndValues = keysAndValues.replace(/\\,/g, ","); // un-escape commas
keysAndValues = keysAndValues.replace(/\\=/g, "="); // un-escape equals signs
const args = keysAndValues.split(keyValueEntrySplitKey);
for (const arg of args) {
const [key, value] = arg.split(valueSplitKey);
parsedObject[key] = value;
}
return parsedObject;
}
/**
* Render a mustache template based on arguments from the user
* @param {string} template - the mustache-style template string into which you would like to insert your values
* @param {string} keysAndValues - a string in the format key1=value1,key2=value2,key3=value3.
* Note: the key names are case sensitive
* @returns {string} - the rendered template
* @throws an Error if the keysAndValues are not in the expected format
*/
static renderTemplateFromKeyValueArguments(template, keysAndValues) {
const mustacheValues = TextUtils.parseKeyValueString(keysAndValues);
return TextUtils.formatMessage(template, mustacheValues);
}
}
exports.TextUtils = TextUtils;
TextUtils.DEFAULT_WRAP_WIDTH = 80;
TextUtils.AVAILABLE_CHALK_COLORS = ["red", "magenta", "blue", "green", "grey", "yellow", "cyan"];
//# sourceMappingURL=TextUtils.js.map
;