symfony-style-console
Version:
Use the style and utilities of the Symfony Console in Node.js
515 lines (514 loc) • 17.5 kB
JavaScript
/**
* Count occurrences of `needle` in `haystack`. Doesn't count overlapped substrings.
*
* @param haystack The string to search in
* @param needle The substring to search for
* @param offset The offset where to start counting. If the offset is negative, counting starts from the end of the string.
* @param length The maximum length after the specified offset to search for the substring. A negative length counts from the end of haystack.
*/
export function countOccurences(haystack, needle, offset = 0, length = 0) {
let count = 0;
if (!needle.length) {
return false;
}
offset--;
while ((offset = haystack.indexOf(needle, offset + 1)) !== -1) {
if (length > 0 && offset + needle.length > length) {
return false;
}
count++;
}
return count;
}
/**
* Generates an array containing the integer values starting with `from` and ending with `to`.
*
* @param from The lower bound
* @param to The upper bound
*/
export function range(from, to) {
from = Math.round(from);
to = Math.round(to);
const arr = [];
if (from < to) {
for (let i = from; i <= to; i++)
arr.push(i);
}
else {
for (let i = to; i >= from; i--)
arr.push(i);
}
return arr;
}
/**
* Formats a string.
*
* This is a port of PHP's [`sprintf`](https://secure.php.net/manual/de/function.sprintf.php) function.
*
* @param format The formatting template
* @param args The values to merge into the template
*/
export function sprintf(format, ...args) {
let regex = /%%|%(\d+\$)?([-+'#0 ]*)(\*\d+\$|\*|\d+)?(?:\.(\*\d+\$|\*|\d+))?([scboxXuideEfFgG])/g;
const a = args.map(arg => String(arg));
let i = 0;
let _pad = function (str, len, chr = ' ', leftJustify) {
let padding = str.length >= len ? '' : new Array((1 + len - str.length) >>> 0).join(chr);
return leftJustify ? str + padding : padding + str;
};
let justify = function (value, prefix, leftJustify, minWidth, zeroPad, customPadChar) {
let diff = minWidth - value.length;
if (diff > 0) {
if (leftJustify || !zeroPad) {
value = _pad(value, minWidth, customPadChar, leftJustify);
}
else {
value = [
value.slice(0, prefix.length),
_pad('', diff, '0', true),
value.slice(prefix.length)
].join('');
}
}
return value;
};
const prefixValues = {
'2': '0b',
'8': '0',
'16': '0x'
};
let _formatBaseX = function (value, base, prefix, leftJustify, minWidth, precision, zeroPad) {
// Note: casts negative numbers to positive ones
value = +value;
let number = value >>> 0;
prefix = (prefix && number && prefixValues[String(base)]) || '';
const strValue = prefix + _pad(number.toString(+base), precision || 0, '0', false);
return justify(strValue, prefix, leftJustify, minWidth, zeroPad);
};
// _formatString()
let _formatString = function (value, leftJustify, minWidth, precision, zeroPad, customPadChar) {
if (precision !== null && precision !== undefined) {
value = value.slice(0, precision);
}
return justify(value, '', leftJustify, minWidth, zeroPad, customPadChar);
};
// doFormat()
// RegEx replacer
let doFormat = function (substring, valueIndex, flags, minWidth, precision, type) {
let number, prefix, method, textTransform, value;
if (substring === '%%') {
return '%';
}
// parse flags
let leftJustify = false;
let positivePrefix = '';
let zeroPad = false;
let prefixBaseX = false;
let customPadChar = ' ';
let flagsl = flags.length;
let j;
for (j = 0; j < flagsl; j++) {
switch (flags.charAt(j)) {
case ' ':
positivePrefix = ' ';
break;
case '+':
positivePrefix = '+';
break;
case '-':
leftJustify = true;
break;
case "'":
customPadChar = flags.charAt(j + 1);
break;
case '0':
zeroPad = true;
customPadChar = '0';
break;
case '#':
prefixBaseX = true;
break;
}
}
// parameters may be null, undefined, empty-string or real valued
// we want to ignore null, undefined and empty-string values
if (typeof minWidth === 'number') {
}
else if (!minWidth) {
minWidth = 0;
}
else if (minWidth === '*') {
minWidth = +a[i++];
}
else if (minWidth.charAt(0) === '*') {
minWidth = +a[+minWidth.slice(1, -1)];
}
else {
minWidth = +minWidth;
}
// Note: undocumented perl feature:
if (minWidth < 0) {
minWidth = -minWidth;
leftJustify = true;
}
if (!isFinite(minWidth)) {
throw new Error('sprintf: (minimum-)width must be finite');
}
if (typeof precision === 'number') {
}
else if (!precision) {
precision = 'fFeE'.indexOf(type) > -1 ? 6 : type === 'd' ? 0 : undefined;
}
else if (precision === '*') {
precision = +a[i++];
}
else if (precision.charAt(0) === '*') {
precision = +a[+precision.slice(1, -1)];
}
else {
precision = +precision;
}
// grab value using valueIndex if required?
value = valueIndex ? a[+String(valueIndex).slice(0, -1)] : a[i++];
switch (type) {
case 's':
return _formatString(value, leftJustify, minWidth, precision, zeroPad, customPadChar);
case 'c':
return _formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, zeroPad);
case 'b':
return _formatBaseX(value, 2, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
case 'o':
return _formatBaseX(value, 8, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
case 'x':
return _formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
case 'X':
return _formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad).toUpperCase();
case 'u':
return _formatBaseX(value, 10, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
case 'i':
case 'd':
number = +value || 0;
// Plain Math.round doesn't just truncate
number = Math.round(number - (number % 1));
prefix = number < 0 ? '-' : positivePrefix;
value = prefix + _pad(String(Math.abs(number)), precision, '0', false);
return justify(value, prefix, leftJustify, minWidth, zeroPad);
case 'e':
case 'E':
case 'f': // @todo: Should handle locales (as per setlocale)
case 'F':
case 'g':
case 'G':
number = +value;
prefix = number < 0 ? '-' : positivePrefix;
const methods = [
'toExponential',
'toFixed',
'toPrecision'
];
method = methods['efg'.indexOf(type.toLowerCase())];
const transforms = ['toString', 'toUpperCase'];
textTransform = transforms['eEfFgG'.indexOf(type) % 2];
value = prefix + Math.abs(number)[method](precision);
return justify(value, prefix, leftJustify, minWidth, zeroPad)[textTransform]();
default:
return substring;
}
};
return format.replace(regex, doFormat);
}
/**
* Creates a human-friendly representation of a number of bytes.
*
* @param memory The number of bytes to format
*/
export function formatMemory(memory) {
if (memory >= 1024 * 1024 * 1024) {
return sprintf('%.1f GiB', memory / 1024 / 1024 / 1024);
}
if (memory >= 1024 * 1024) {
return sprintf('%.1f MiB', memory / 1024 / 1024);
}
if (memory >= 1024) {
return sprintf('%d KiB', memory / 1024);
}
return sprintf('%d B', memory);
}
/**
* Removes `<...>` formatting and ANSI escape sequences from a string.
*
* @param formatter The formatter instance in charge to resolve `<...>` formatting
* @param str The string to perform the removing on
*/
export function removeDecoration(formatter, str) {
const isDecorated = formatter.isDecorated();
formatter.setDecorated(false);
// Resolve <...> formatting
str = formatter.format(str);
// Remove ANSI-formatted characters
str = str.replace(/\033\[[^m]*m/g, '');
formatter.setDecorated(isDecorated);
return str;
}
/**
* Get the length of a string ignoring `<...>` formatting and ANSI escape sequences.
*
* @param formatter The formatter instance in charge to resolve `<...>` formatting
* @param str The string whose length to determine
*/
export function lengthWithoutDecoration(formatter, str) {
return removeDecoration(formatter, str).length;
}
/**
* Creates an object literal with key/value pairs of the given `obj` swapped.
*
* @param obj The object whose keys and values to use
*/
export function flipObject(obj) {
const flipped = Object.create(null);
for (const key in obj)
flipped[obj[key]] = key;
return flipped;
}
/**
* Checks if `item` is contained by `arr`.
*
* @param arr The array to search in
* @param item The item to search for
*/
export function arrContains(arr, item) {
if (Array.prototype.includes) {
return arr.includes(item);
}
else {
return arr.indexOf(item) !== -1;
}
}
/**
* Creates a human-friendly representation of a number of seconds.
*
* @param secs The number of seconds to format
*/
export function formatTime(secs) {
const timeFormats = [
[0, '< 1 sec'],
[1, '1 sec'],
[2, 'secs', 1],
[60, '1 min'],
[120, 'mins', 60],
[3600, '1 hr'],
[7200, 'hrs', 3600],
[86400, '1 day'],
[172800, 'days', 86400]
];
for (let index = 0; index < timeFormats.length; index++) {
const format = timeFormats[index];
if (secs >= format[0]) {
if ((timeFormats[index + 1] && secs < timeFormats[index + 1][0]) ||
index == timeFormats.length - 1) {
if (2 == format.length) {
return format[1];
}
return `${Math.floor(secs / format[2])} ${format[1]}`;
}
}
}
}
/**
* Pads a string to a certain length with another string.
*
* @param input The string to pad
* @param padLength The desired length of the resulting string
* @param padString The string to use as padding material
* @param padType Where to pad the string
*/
export function strPad(input, padLength, padString, padType = 'STR_PAD_RIGHT') {
let half = '';
let padToGo;
let _strPadRepeater = function (s, len) {
let collect = '';
while (collect.length < len) {
collect += s;
}
collect = collect.substr(0, len);
return collect;
};
input += '';
padString = padString !== undefined ? padString : ' ';
if (padType !== 'STR_PAD_LEFT' &&
padType !== 'STR_PAD_RIGHT' &&
padType !== 'STR_PAD_BOTH') {
padType = 'STR_PAD_RIGHT';
}
if ((padToGo = padLength - input.length) > 0) {
if (padType === 'STR_PAD_LEFT') {
input = _strPadRepeater(padString, padToGo) + input;
}
else if (padType === 'STR_PAD_RIGHT') {
input = input + _strPadRepeater(padString, padToGo);
}
else if (padType === 'STR_PAD_BOTH') {
half = _strPadRepeater(padString, Math.ceil(padToGo / 2));
input = half + input + half;
input = input.substr(0, padLength);
}
}
return input;
}
/**
* Replaces elements from passed arrays into the first array recursively. (non-destructive)
*
* @param arr The array to patch
* @param args The arrays to merge into `arr`
*/
export function arrayReplaceRecursive(arr, ...args) {
let i = 0;
let p = '';
if (!args.length) {
throw new Error('There should be at least 2 arguments passed to array_replace_recursive()');
}
// Although docs state that the arguments are passed in by reference,
// it seems they are not altered, but rather the copy that is returned
// So we make a copy here, instead of acting on arr itself
const retArr = [];
for (p in arr) {
if (typeof arr[p] === 'undefined')
continue;
retArr[+p] = arr[p];
}
for (i = 0; i < args.length; i++) {
for (p in args[i]) {
if (retArr[+p] && typeof retArr[+p] === 'object') {
retArr[+p] = arrayReplaceRecursive(retArr[+p], arguments[i][+p]);
}
else {
retArr[+p] = arguments[i][+p];
}
}
}
return retArr;
}
/**
* Creates an array filled with a value.
*
* @param startIndex The index to start filling at. Previous values will be `undefined`.
* @param num The number elements to insert
* @param value The element to fill the array with
*/
export function arrayFill(startIndex, num, value) {
let key;
let tmpArr = [];
if (!isNaN(startIndex) && !isNaN(num)) {
for (key = 0; key < num; key++) {
tmpArr[key + startIndex] = value;
}
}
return tmpArr;
}
/**
* Cuts a string into an array of chunks of given length.
*
* @param string The string to cut
* @param splitLength The length of the resulting chunks
*/
export function chunkString(string, splitLength = 1) {
if (string === null || splitLength < 1) {
return false;
}
string += '';
let chunks = [];
let pos = 0;
let len = string.length;
while (pos < len) {
chunks.push(string.slice(pos, (pos += splitLength)));
}
return chunks;
}
/**
* Strips HTML tags from a string.
*
* @param input The string to sanitize
* @param allowed A string containing a set of allowed tags. Example: `<a><strong><em>`
*/
export function stripTags(input, allowed = '') {
allowed = (((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join('');
let tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
let comments = /<!--[\s\S]*?-->/gi;
return input.replace(comments, '').replace(tags, function ($0, $1) {
return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : '';
});
}
/**
* Trims the end of a string.
*
* @param str The string to trim
* @param charlist The characters to trim. Everything that can go into a regex character class is allowed.
*/
export function trimEnd(str, charlist = ' \\s\u00A0') {
charlist = String(charlist).replace(/([[\]().?/*{}+$^:])/g, '\\$1');
let re = new RegExp('[' + charlist.replace(/\\/g, '\\\\') + ']+$', 'g');
return (str + '').replace(re, '');
}
/**
* Non-recursively replaces a set of values inside a string.
*
* @param str The string to perform replacements on
* @param replacePairs An object that maps strings to their respective replacements
*/
export function safeReplace(str, replacePairs) {
'use strict';
str = String(str);
let key, re;
for (key in replacePairs) {
if (replacePairs.hasOwnProperty(key)) {
re = new RegExp(key, 'g');
str = str.replace(re, replacePairs[key]);
}
}
return str;
}
/**
* Wraps a string to a given number of characters.
*
* Note that, as opposed to PHP's wordwrap, this always cuts words which are too long for one line.
*
* @param str The string to wrap
* @param width The maximum length of a line
* @param breakSequence The character(s) to use as line breaks
*/
export function wordwrap(str, width = 75, breakSequence = '\n') {
let j, l, s, r;
str += '';
if (width < 1) {
return str;
}
let splitted = str.split(/\r\n|\n|\r/);
for (let i = 0; i < splitted.length; i++) {
let rest = splitted[i];
const partials = [];
while (rest.length > width) {
let line = rest.slice(0, width);
if (rest.slice(width).match(/^\s/)) {
rest = rest.slice(line.length + 1);
}
else if (line.match(/\s/)) {
line = line.slice(0, line.match(/\s(?=[^\s]*$)/).index);
rest = rest.slice(line.length + 1);
}
else {
rest = rest.slice(line.length);
}
partials.push(line);
}
if (rest.length)
partials.push(rest);
splitted[i] = partials.join(breakSequence);
}
return splitted.join(breakSequence);
}
/**
* Returns the current timestamp in seconds.
*/
export function time() {
return Math.floor(Date.now() / 1000);
}