Fixed #1 - Fixed the whatsnew-** script for loading release notes into builds
Added test to ensure that release notes are parsed properly Updated to latest dependencies to fix security vulnerability Updated GH Actions Workflow to run test before updating tag Updated README.md to reflect proper whatsnew documentation.
This commit is contained in:
168
node_modules/prompts/lib/elements/autocomplete.js
generated
vendored
168
node_modules/prompts/lib/elements/autocomplete.js
generated
vendored
@@ -2,8 +2,8 @@
|
||||
|
||||
const color = require('kleur');
|
||||
const Prompt = require('./prompt');
|
||||
const { cursor } = require('sisteransi');
|
||||
const { style, clear, figures, strip } = require('../util');
|
||||
const { erase, cursor } = require('sisteransi');
|
||||
const { style, clear, figures, wrap, entriesToDisplay } = require('../util');
|
||||
|
||||
const getVal = (arr, i) => arr[i] && (arr[i].value || arr[i].title || arr[i]);
|
||||
const getTitle = (arr, i) => arr[i] && (arr[i].title || arr[i].value || arr[i]);
|
||||
@@ -37,13 +37,9 @@ class AutocompletePrompt extends Prompt {
|
||||
? opts.initial
|
||||
: getIndex(opts.choices, opts.initial);
|
||||
this.select = this.initial || opts.cursor || 0;
|
||||
this.fallback = opts.fallback || (
|
||||
opts.initial !== undefined ?
|
||||
`${figures.pointerSmall} ${getTitle(this.choices, this.initial)}` :
|
||||
`${figures.pointerSmall} ${opts.noMatches || 'no matches found'}`
|
||||
);
|
||||
this.suggestions = [[]];
|
||||
this.page = 0;
|
||||
this.i18n = { noMatches: opts.noMatches || 'no matches found' };
|
||||
this.fallback = opts.fallback || this.initial;
|
||||
this.suggestions = [];
|
||||
this.input = '';
|
||||
this.limit = opts.limit || 10;
|
||||
this.cursor = 0;
|
||||
@@ -56,15 +52,24 @@ class AutocompletePrompt extends Prompt {
|
||||
this.render();
|
||||
}
|
||||
|
||||
set fallback(fb) {
|
||||
this._fb = Number.isSafeInteger(parseInt(fb)) ? parseInt(fb) : fb;
|
||||
}
|
||||
|
||||
get fallback() {
|
||||
let choice;
|
||||
if (typeof this._fb === 'number')
|
||||
choice = this.choices[this._fb];
|
||||
else if (typeof this._fb === 'string')
|
||||
choice = { title: this._fb };
|
||||
return choice || this._fb || { title: this.i18n.noMatches };
|
||||
}
|
||||
|
||||
moveSelect(i) {
|
||||
this.select = i;
|
||||
if (this.suggestions[this.page].length > 0) {
|
||||
this.value = getVal(this.suggestions[this.page], i);
|
||||
} else {
|
||||
this.value = this.initial !== undefined
|
||||
? getVal(this.choices, this.initial)
|
||||
: null;
|
||||
}
|
||||
if (this.suggestions.length > 0)
|
||||
this.value = getVal(this.suggestions, i);
|
||||
else this.value = this.fallback.value;
|
||||
this.fire();
|
||||
}
|
||||
|
||||
@@ -74,26 +79,8 @@ class AutocompletePrompt extends Prompt {
|
||||
|
||||
if (this.completing !== p) return;
|
||||
this.suggestions = suggestions
|
||||
.map((s, i, arr) => ({title: getTitle(arr, i), value: getVal(arr, i)}))
|
||||
.reduce((arr, sug) => {
|
||||
if (arr[arr.length - 1].length < this.limit)
|
||||
arr[arr.length - 1].push(sug);
|
||||
else arr.push([sug]);
|
||||
return arr;
|
||||
}, [[]]);
|
||||
this.isFallback = false;
|
||||
.map((s, i, arr) => ({ title: getTitle(arr, i), value: getVal(arr, i), description: s.description }));
|
||||
this.completing = false;
|
||||
if (!this.suggestions[this.page])
|
||||
this.page = 0;
|
||||
|
||||
if (!this.suggestions.length && this.fallback) {
|
||||
const index = getIndex(this.choices, this.fallback);
|
||||
this.suggestions = [[]];
|
||||
if (index !== undefined)
|
||||
this.suggestions[0].push({ title: getTitle(this.choices, index), value: getVal(this.choices, index) });
|
||||
this.isFallback = true;
|
||||
}
|
||||
|
||||
const l = Math.max(suggestions.length - 1, 0);
|
||||
this.moveSelect(Math.min(l, this.select));
|
||||
|
||||
@@ -126,7 +113,7 @@ class AutocompletePrompt extends Prompt {
|
||||
this.close();
|
||||
}
|
||||
|
||||
_(c, key) { // TODO on ctrl+# go to page #
|
||||
_(c, key) {
|
||||
let s1 = this.input.slice(0, this.cursor);
|
||||
let s2 = this.input.slice(this.cursor);
|
||||
this.input = `${s1}${c}${s2}`;
|
||||
@@ -146,12 +133,12 @@ class AutocompletePrompt extends Prompt {
|
||||
}
|
||||
|
||||
deleteForward() {
|
||||
if(this.cursor*this.scale >= this.rendered.length) return this.bell();
|
||||
let s1 = this.input.slice(0, this.cursor);
|
||||
let s2 = this.input.slice(this.cursor+1);
|
||||
this.input = `${s1}${s2}`;
|
||||
this.complete(this.render);
|
||||
this.render();
|
||||
if(this.cursor*this.scale >= this.rendered.length) return this.bell();
|
||||
let s1 = this.input.slice(0, this.cursor);
|
||||
let s2 = this.input.slice(this.cursor+1);
|
||||
this.input = `${s1}${s2}`;
|
||||
this.complete(this.render);
|
||||
this.render();
|
||||
}
|
||||
|
||||
first() {
|
||||
@@ -160,7 +147,7 @@ class AutocompletePrompt extends Prompt {
|
||||
}
|
||||
|
||||
last() {
|
||||
this.moveSelect(this.suggestions[this.page].length - 1);
|
||||
this.moveSelect(this.suggestions.length - 1);
|
||||
this.render();
|
||||
}
|
||||
|
||||
@@ -171,32 +158,25 @@ class AutocompletePrompt extends Prompt {
|
||||
}
|
||||
|
||||
down() {
|
||||
if (this.select >= this.suggestions[this.page].length - 1) return this.bell();
|
||||
if (this.select >= this.suggestions.length - 1) return this.bell();
|
||||
this.moveSelect(this.select + 1);
|
||||
this.render();
|
||||
}
|
||||
|
||||
next() {
|
||||
if (this.select === this.suggestions[this.page].length - 1) {
|
||||
this.page = (this.page + 1) % this.suggestions.length;
|
||||
if (this.select === this.suggestions.length - 1) {
|
||||
this.moveSelect(0);
|
||||
} else this.moveSelect(this.select + 1);
|
||||
this.render();
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
if (this.page >= this.suggestions.length - 1)
|
||||
return this.bell();
|
||||
this.page++;
|
||||
this.moveSelect(0);
|
||||
this.moveSelect(Math.min(this.select + this.limit, this.suggestions.length - 1));
|
||||
this.render();
|
||||
}
|
||||
|
||||
prevPage() {
|
||||
if (this.page <= 0)
|
||||
return this.bell();
|
||||
this.page--;
|
||||
this.moveSelect(0);
|
||||
this.moveSelect(Math.max(this.select - this.limit, 0));
|
||||
this.render();
|
||||
}
|
||||
|
||||
@@ -212,52 +192,50 @@ class AutocompletePrompt extends Prompt {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.closed) return;
|
||||
super.render();
|
||||
if (this.lineCount) this.out.write(cursor.down(this.lineCount));
|
||||
|
||||
let prompt = color.bold(`${style.symbol(this.done, this.aborted)} ${this.msg} `)
|
||||
+ `${style.delimiter(this.completing)} `;
|
||||
let length = strip(prompt).length;
|
||||
|
||||
if (this.done && this.suggestions[this.page][this.select]) {
|
||||
prompt += `${this.suggestions[this.page][this.select].title}`;
|
||||
} else {
|
||||
this.rendered = `${this.transform.render(this.input)}`;
|
||||
length += this.rendered.length;
|
||||
prompt += this.rendered;
|
||||
}
|
||||
|
||||
if (!this.done) {
|
||||
this.lineCount = this.suggestions[this.page].length;
|
||||
let suggestions = this.suggestions[this.page].reduce((acc, item, i) =>
|
||||
acc + `\n${i === this.select ? color.cyan(item.title) : item.title}`, '');
|
||||
if (suggestions && !this.isFallback) {
|
||||
prompt += suggestions;
|
||||
if (this.suggestions.length > 1) {
|
||||
this.lineCount++;
|
||||
prompt += color.blue(`\nPage ${this.page+1}/${this.suggestions.length}`);
|
||||
}
|
||||
} else {
|
||||
const fallbackIndex = getIndex(this.choices, this.fallback);
|
||||
const fallbackTitle = fallbackIndex !== undefined
|
||||
? getTitle(this.choices, fallbackIndex)
|
||||
: this.fallback;
|
||||
prompt += `\n${color.gray(fallbackTitle)}`;
|
||||
this.lineCount++;
|
||||
renderOption(v, hovered, isStart, isEnd) {
|
||||
let desc;
|
||||
let prefix = isStart ? figures.arrowUp : isEnd ? figures.arrowDown : ' ';
|
||||
let title = hovered ? color.cyan().underline(v.title) : v.title;
|
||||
prefix = (hovered ? color.cyan(figures.pointer) + ' ' : ' ') + prefix;
|
||||
if (v.description) {
|
||||
desc = ` - ${v.description}`;
|
||||
if (prefix.length + title.length + desc.length >= this.out.columns
|
||||
|| v.description.split(/\r?\n/).length > 1) {
|
||||
desc = '\n' + wrap(v.description, { margin: 3, width: this.out.columns })
|
||||
}
|
||||
}
|
||||
return prefix + ' ' + title + color.gray(desc || '');
|
||||
}
|
||||
|
||||
this.out.write(this.clear + prompt);
|
||||
this.clear = clear(prompt);
|
||||
render() {
|
||||
if (this.closed) return;
|
||||
if (this.firstRender) this.out.write(cursor.hide);
|
||||
else this.out.write(clear(this.outputText));
|
||||
super.render();
|
||||
|
||||
if (this.lineCount && !this.done) {
|
||||
let pos = cursor.up(this.lineCount);
|
||||
pos += cursor.left+cursor.to(length);
|
||||
pos += cursor.move(-this.rendered.length+this.cursor*this.scale);
|
||||
this.out.write(pos);
|
||||
let { startIndex, endIndex } = entriesToDisplay(this.select, this.choices.length, this.limit);
|
||||
|
||||
this.outputText = [
|
||||
color.bold(style.symbol(this.done, this.aborted)),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(this.completing),
|
||||
this.done && this.suggestions[this.select]
|
||||
? this.suggestions[this.select].title
|
||||
: this.rendered = this.transform.render(this.input)
|
||||
].join(' ');
|
||||
|
||||
if (!this.done) {
|
||||
const suggestions = this.suggestions
|
||||
.slice(startIndex, endIndex)
|
||||
.map((item, i) => this.renderOption(item,
|
||||
this.select === i + startIndex,
|
||||
i === 0 && startIndex > 0,
|
||||
i + startIndex === endIndex - 1 && endIndex < this.choices.length))
|
||||
.join('\n');
|
||||
this.outputText += `\n` + (suggestions || color.gray(this.fallback.title));
|
||||
}
|
||||
|
||||
this.out.write(erase.line + cursor.to(0) + this.outputText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
node_modules/prompts/lib/elements/autocompleteMultiselect.js
generated
vendored
21
node_modules/prompts/lib/elements/autocompleteMultiselect.js
generated
vendored
@@ -119,17 +119,23 @@ class AutocompleteMultiselectPrompt extends MultiselectPrompt {
|
||||
this.handleSpaceToggle();
|
||||
} else {
|
||||
this.handleInputChange(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderInstructions() {
|
||||
return `
|
||||
if (this.instructions === undefined || this.instructions) {
|
||||
if (typeof this.instructions === 'string') {
|
||||
return this.instructions;
|
||||
}
|
||||
return `
|
||||
Instructions:
|
||||
${figures.arrowUp}/${figures.arrowDown}: Highlight option
|
||||
${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection
|
||||
[a,b,c]/delete: Filter choices
|
||||
enter/return: Complete answer
|
||||
`
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
renderCurrentInput() {
|
||||
@@ -146,13 +152,12 @@ Filtered results for: ${this.inputValue ? this.inputValue : color.gray('Enter so
|
||||
|
||||
renderDoneOrInstructions() {
|
||||
if (this.done) {
|
||||
const selected = this.value
|
||||
return this.value
|
||||
.filter(e => e.selected)
|
||||
.map(v => v.title)
|
||||
.join(', ');
|
||||
return selected;
|
||||
}
|
||||
|
||||
|
||||
const output = [color.gray(this.hint), this.renderInstructions(), this.renderCurrentInput()];
|
||||
|
||||
if (this.filteredOptions.length && this.filteredOptions[this.cursor].disabled) {
|
||||
@@ -167,14 +172,14 @@ Filtered results for: ${this.inputValue ? this.inputValue : color.gray('Enter so
|
||||
super.render();
|
||||
|
||||
// print prompt
|
||||
|
||||
|
||||
let prompt = [
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(false),
|
||||
this.renderDoneOrInstructions()
|
||||
].join(' ');
|
||||
|
||||
|
||||
if (this.showMinError) {
|
||||
prompt += color.red(`You must select a minimum of ${this.minSelected} choices.`);
|
||||
this.showMinError = false;
|
||||
|
||||
26
node_modules/prompts/lib/elements/confirm.js
generated
vendored
26
node_modules/prompts/lib/elements/confirm.js
generated
vendored
@@ -1,6 +1,6 @@
|
||||
const color = require('kleur');
|
||||
const Prompt = require('./prompt');
|
||||
const { style } = require('../util');
|
||||
const { style, clear } = require('../util');
|
||||
const { erase, cursor } = require('sisteransi');
|
||||
|
||||
/**
|
||||
@@ -67,20 +67,18 @@ class ConfirmPrompt extends Prompt {
|
||||
render() {
|
||||
if (this.closed) return;
|
||||
if (this.firstRender) this.out.write(cursor.hide);
|
||||
else this.out.write(clear(this.outputText));
|
||||
super.render();
|
||||
|
||||
this.out.write(
|
||||
erase.line +
|
||||
cursor.to(0) +
|
||||
[
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(this.done),
|
||||
this.done
|
||||
? this.value ? this.yesMsg : this.noMsg
|
||||
: color.gray(this.initialValue ? this.yesOption : this.noOption)
|
||||
].join(' ')
|
||||
);
|
||||
|
||||
this.outputText = [
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(this.done),
|
||||
this.done ? (this.value ? this.yesMsg : this.noMsg)
|
||||
: color.gray(this.initialValue ? this.yesOption : this.noOption)
|
||||
].join(' ');
|
||||
|
||||
this.out.write(erase.line + cursor.to(0) + this.outputText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
node_modules/prompts/lib/elements/date.js
generated
vendored
32
node_modules/prompts/lib/elements/date.js
generated
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
const color = require('kleur');
|
||||
const Prompt = require('./prompt');
|
||||
const { style, clear, figures, strip } = require('../util');
|
||||
const { style, clear, figures } = require('../util');
|
||||
const { erase, cursor } = require('sisteransi');
|
||||
const { DatePart, Meridiem, Day, Hours, Milliseconds, Minutes, Month, Seconds, Year } = require('../dateparts');
|
||||
|
||||
@@ -26,6 +26,7 @@ const dfltLocales = {
|
||||
weekdaysShort: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(',')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DatePrompt Base Element
|
||||
* @param {Object} opts Options
|
||||
@@ -108,7 +109,7 @@ class DatePrompt extends Prompt {
|
||||
this.out.write('\n');
|
||||
this.close();
|
||||
}
|
||||
|
||||
|
||||
async validate() {
|
||||
let valid = await this.validator(this.value);
|
||||
if (typeof valid === 'string') {
|
||||
@@ -179,34 +180,25 @@ class DatePrompt extends Prompt {
|
||||
render() {
|
||||
if (this.closed) return;
|
||||
if (this.firstRender) this.out.write(cursor.hide);
|
||||
else this.out.write(erase.lines(1));
|
||||
else this.out.write(clear(this.outputText));
|
||||
super.render();
|
||||
let clear = erase.line + (this.lines ? erase.down(this.lines) : '') + cursor.to(0);
|
||||
this.lines = 0;
|
||||
|
||||
let error = '';
|
||||
if (this.error) {
|
||||
let lines = this.errorMsg.split('\n');
|
||||
error = lines.reduce((a, l, i) => a + `\n${i ? ` ` : figures.pointerSmall} ${color.red().italic(l)}`, ``);
|
||||
this.lines = lines.length;
|
||||
}
|
||||
|
||||
// Print prompt
|
||||
let prompt = [
|
||||
this.outputText = [
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(false),
|
||||
this.parts.reduce((arr, p, idx) => arr.concat(idx === this.cursor && !this.done ? color.cyan().underline(p.toString()) : p), [])
|
||||
.join(''),
|
||||
.join('')
|
||||
].join(' ');
|
||||
|
||||
let position = '';
|
||||
if (this.lines) {
|
||||
position += cursor.up(this.lines);
|
||||
position += cursor.left+cursor.to(strip(prompt).length);
|
||||
|
||||
// Print error
|
||||
if (this.error) {
|
||||
this.outputText += this.errorMsg.split('\n').reduce(
|
||||
(a, l, i) => a + `\n${i ? ` ` : figures.pointerSmall} ${color.red().italic(l)}`, ``);
|
||||
}
|
||||
|
||||
this.out.write(clear+prompt+error+position);
|
||||
this.out.write(erase.line + cursor.to(0) + this.outputText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
107
node_modules/prompts/lib/elements/multiselect.js
generated
vendored
107
node_modules/prompts/lib/elements/multiselect.js
generated
vendored
@@ -3,7 +3,7 @@
|
||||
const color = require('kleur');
|
||||
const { cursor } = require('sisteransi');
|
||||
const Prompt = require('./prompt');
|
||||
const { clear, figures, style } = require('../util');
|
||||
const { clear, figures, style, wrap, entriesToDisplay } = require('../util');
|
||||
|
||||
/**
|
||||
* MultiselectPrompt Base Element
|
||||
@@ -14,6 +14,7 @@ const { clear, figures, style } = require('../util');
|
||||
* @param {String} [opts.warn] Hint shown for disabled choices
|
||||
* @param {Number} [opts.max] Max choices
|
||||
* @param {Number} [opts.cursor=0] Cursor start position
|
||||
* @param {Number} [opts.optionsPerPage=10] Max options to display at once
|
||||
* @param {Stream} [opts.stdin] The Readable stream to listen to
|
||||
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
|
||||
*/
|
||||
@@ -28,12 +29,15 @@ class MultiselectPrompt extends Prompt {
|
||||
this.minSelected = opts.min;
|
||||
this.showMinError = false;
|
||||
this.maxChoices = opts.max;
|
||||
this.value = opts.choices.map((ch, idx) => {
|
||||
this.instructions = opts.instructions;
|
||||
this.optionsPerPage = opts.optionsPerPage || 10;
|
||||
this.value = opts.choices.map((ch, idx) => {
|
||||
if (typeof ch === 'string')
|
||||
ch = {title: ch, value: idx};
|
||||
return {
|
||||
title: ch && (ch.title || ch.value || ch),
|
||||
value: ch && (ch.value || idx),
|
||||
description: ch && ch.description,
|
||||
value: ch && (ch.value === undefined ? idx : ch.value),
|
||||
selected: ch && ch.selected,
|
||||
disabled: ch && ch.disabled
|
||||
};
|
||||
@@ -136,54 +140,81 @@ class MultiselectPrompt extends Prompt {
|
||||
}
|
||||
}
|
||||
|
||||
toggleAll() {
|
||||
if (this.maxChoices !== undefined || this.value[this.cursor].disabled) {
|
||||
return this.bell();
|
||||
}
|
||||
|
||||
const newSelected = !this.value[this.cursor].selected;
|
||||
this.value.filter(v => !v.disabled).forEach(v => v.selected = newSelected);
|
||||
this.render();
|
||||
}
|
||||
|
||||
_(c, key) {
|
||||
if (c === ' ') {
|
||||
this.handleSpaceToggle();
|
||||
} else if (c === 'a') {
|
||||
this.toggleAll();
|
||||
} else {
|
||||
return this.bell();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderInstructions() {
|
||||
return `
|
||||
Instructions:
|
||||
${figures.arrowUp}/${figures.arrowDown}: Highlight option
|
||||
${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection
|
||||
enter/return: Complete answer
|
||||
`
|
||||
if (this.instructions === undefined || this.instructions) {
|
||||
if (typeof this.instructions === 'string') {
|
||||
return this.instructions;
|
||||
}
|
||||
return '\nInstructions:\n'
|
||||
+ ` ${figures.arrowUp}/${figures.arrowDown}: Highlight option\n`
|
||||
+ ` ${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection\n`
|
||||
+ (this.maxChoices === undefined ? ` a: Toggle all\n` : '')
|
||||
+ ` enter/return: Complete answer`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
renderOption(cursor, v, i) {
|
||||
let title;
|
||||
if (v.disabled) title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
||||
else title = cursor === i ? color.cyan().underline(v.title) : v.title;
|
||||
return (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ' + title
|
||||
renderOption(cursor, v, i, arrowIndicator) {
|
||||
const prefix = (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ' + arrowIndicator + ' ';
|
||||
let title, desc;
|
||||
|
||||
if (v.disabled) {
|
||||
title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
||||
} else {
|
||||
title = cursor === i ? color.cyan().underline(v.title) : v.title;
|
||||
if (cursor === i && v.description) {
|
||||
desc = ` - ${v.description}`;
|
||||
if (prefix.length + title.length + desc.length >= this.out.columns
|
||||
|| v.description.split(/\r?\n/).length > 1) {
|
||||
desc = '\n' + wrap(v.description, { margin: prefix.length, width: this.out.columns });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prefix + title + color.gray(desc || '');
|
||||
}
|
||||
|
||||
// shared with autocompleteMultiselect
|
||||
paginateOptions(options) {
|
||||
const c = this.cursor;
|
||||
let styledOptions = options.map((v, i) => this.renderOption(c, v, i));
|
||||
const numOfOptionsToRender = 10; // if needed, can add an option to change this.
|
||||
|
||||
let scopedOptions = styledOptions;
|
||||
let hint = '';
|
||||
if (styledOptions.length === 0) {
|
||||
if (options.length === 0) {
|
||||
return color.red('No matches for this query.');
|
||||
} else if (styledOptions.length > numOfOptionsToRender) {
|
||||
let startIndex = c - (numOfOptionsToRender / 2);
|
||||
let endIndex = c + (numOfOptionsToRender / 2);
|
||||
if (startIndex < 0) {
|
||||
startIndex = 0;
|
||||
endIndex = numOfOptionsToRender;
|
||||
} else if (endIndex > options.length) {
|
||||
endIndex = options.length;
|
||||
startIndex = endIndex - numOfOptionsToRender;
|
||||
}
|
||||
scopedOptions = styledOptions.slice(startIndex, endIndex);
|
||||
hint = color.dim('(Move up and down to reveal more choices)');
|
||||
}
|
||||
return '\n' + scopedOptions.join('\n') + '\n' + hint;
|
||||
|
||||
let { startIndex, endIndex } = entriesToDisplay(this.cursor, options.length, this.optionsPerPage);
|
||||
let prefix, styledOptions = [];
|
||||
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
if (i === startIndex && startIndex > 0) {
|
||||
prefix = figures.arrowUp;
|
||||
} else if (i === endIndex - 1 && endIndex < options.length) {
|
||||
prefix = figures.arrowDown;
|
||||
} else {
|
||||
prefix = ' ';
|
||||
}
|
||||
styledOptions.push(this.renderOption(this.cursor, options[i], i, prefix));
|
||||
}
|
||||
|
||||
return '\n' + styledOptions.join('\n');
|
||||
}
|
||||
|
||||
// shared with autocomleteMultiselect
|
||||
@@ -196,13 +227,12 @@ Instructions:
|
||||
|
||||
renderDoneOrInstructions() {
|
||||
if (this.done) {
|
||||
const selected = this.value
|
||||
return this.value
|
||||
.filter(e => e.selected)
|
||||
.map(v => v.title)
|
||||
.join(', ');
|
||||
return selected;
|
||||
}
|
||||
|
||||
|
||||
const output = [color.gray(this.hint), this.renderInstructions()];
|
||||
|
||||
if (this.value[this.cursor].disabled) {
|
||||
@@ -217,7 +247,6 @@ Instructions:
|
||||
super.render();
|
||||
|
||||
// print prompt
|
||||
|
||||
let prompt = [
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
|
||||
43
node_modules/prompts/lib/elements/number.js
generated
vendored
43
node_modules/prompts/lib/elements/number.js
generated
vendored
@@ -1,7 +1,7 @@
|
||||
const color = require('kleur');
|
||||
const Prompt = require('./prompt');
|
||||
const { cursor, erase } = require('sisteransi');
|
||||
const { style, clear, figures, strip } = require('../util');
|
||||
const { style, figures, clear, lines } = require('../util');
|
||||
|
||||
const isNumber = /[0-9]/;
|
||||
const isDef = any => any !== undefined;
|
||||
@@ -119,6 +119,9 @@ class NumberPrompt extends Prompt {
|
||||
|
||||
up() {
|
||||
this.typed = ``;
|
||||
if(this.value === '') {
|
||||
this.value = this.min - this.inc;
|
||||
}
|
||||
if (this.value >= this.max) return this.bell();
|
||||
this.value += this.inc;
|
||||
this.color = `cyan`;
|
||||
@@ -128,6 +131,9 @@ class NumberPrompt extends Prompt {
|
||||
|
||||
down() {
|
||||
this.typed = ``;
|
||||
if(this.value === '') {
|
||||
this.value = this.min + this.inc;
|
||||
}
|
||||
if (this.value <= this.min) return this.bell();
|
||||
this.value -= this.inc;
|
||||
this.color = `cyan`;
|
||||
@@ -139,6 +145,9 @@ class NumberPrompt extends Prompt {
|
||||
let val = this.value.toString();
|
||||
if (val.length === 0) return this.bell();
|
||||
this.value = this.parse((val = val.slice(0, -1))) || ``;
|
||||
if (this.value !== '' && this.value < this.min) {
|
||||
this.value = this.min;
|
||||
}
|
||||
this.color = `cyan`;
|
||||
this.fire();
|
||||
this.render();
|
||||
@@ -170,32 +179,30 @@ class NumberPrompt extends Prompt {
|
||||
|
||||
render() {
|
||||
if (this.closed) return;
|
||||
super.render();
|
||||
let clear = erase.line + (this.lines ? erase.down(this.lines) : ``) + cursor.to(0);
|
||||
this.lines = 0;
|
||||
|
||||
let error = ``;
|
||||
if (this.error) {
|
||||
let lines = this.errorMsg.split(`\n`);
|
||||
error += lines.reduce((a, l, i) => a + `\n${i ? ` ` : figures.pointerSmall} ${color.red().italic(l)}`, ``);
|
||||
this.lines = lines.length;
|
||||
if (!this.firstRender) {
|
||||
if (this.outputError)
|
||||
this.out.write(cursor.down(lines(this.outputError) - 1) + clear(this.outputError));
|
||||
this.out.write(clear(this.outputText));
|
||||
}
|
||||
super.render();
|
||||
this.outputError = '';
|
||||
|
||||
let underline = !this.done || (!this.done && !this.placeholder);
|
||||
let prompt = [
|
||||
// Print prompt
|
||||
this.outputText = [
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(this.done),
|
||||
underline ? color[this.color]().underline(this.rendered) : this.rendered
|
||||
!this.done || (!this.done && !this.placeholder)
|
||||
? color[this.color]().underline(this.rendered) : this.rendered
|
||||
].join(` `);
|
||||
|
||||
let position = ``;
|
||||
if (this.lines) {
|
||||
position += cursor.up(this.lines);
|
||||
position += cursor.left+cursor.to(strip(prompt).length);
|
||||
// Print error
|
||||
if (this.error) {
|
||||
this.outputError += this.errorMsg.split(`\n`)
|
||||
.reduce((a, l, i) => a + `\n${i ? ` ` : figures.pointerSmall} ${color.red().italic(l)}`, ``);
|
||||
}
|
||||
|
||||
this.out.write(clear+prompt+error+position);
|
||||
this.out.write(erase.line + cursor.to(0) + this.outputText + cursor.save + this.outputError + cursor.restore);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
9
node_modules/prompts/lib/elements/prompt.js
generated
vendored
9
node_modules/prompts/lib/elements/prompt.js
generated
vendored
@@ -16,17 +16,16 @@ class Prompt extends EventEmitter {
|
||||
super();
|
||||
|
||||
this.firstRender = true;
|
||||
this.in = opts.in || process.stdin;
|
||||
this.out = opts.out || process.stdout;
|
||||
this.in = opts.stdin || process.stdin;
|
||||
this.out = opts.stdout || process.stdout;
|
||||
this.onRender = (opts.onRender || (() => void 0)).bind(this);
|
||||
|
||||
const rl = readline.createInterface(this.in);
|
||||
readline.emitKeypressEvents(this.in, rl);
|
||||
|
||||
if (this.in.isTTY) this.in.setRawMode(true);
|
||||
|
||||
const isSelect = [ 'SelectPrompt', 'MultiselectPrompt' ].indexOf(this.constructor.name) > -1;
|
||||
const keypress = (str, key) => {
|
||||
let a = action(key);
|
||||
let a = action(key, isSelect);
|
||||
if (a === false) {
|
||||
this._ && this._(str, key);
|
||||
} else if (typeof this[a] === 'function') {
|
||||
|
||||
77
node_modules/prompts/lib/elements/select.js
generated
vendored
77
node_modules/prompts/lib/elements/select.js
generated
vendored
@@ -2,8 +2,8 @@
|
||||
|
||||
const color = require('kleur');
|
||||
const Prompt = require('./prompt');
|
||||
const { style, clear, figures } = require('../util');
|
||||
const { erase, cursor } = require('sisteransi');
|
||||
const { style, clear, figures, wrap, entriesToDisplay } = require('../util');
|
||||
const { cursor } = require('sisteransi');
|
||||
|
||||
/**
|
||||
* SelectPrompt Base Element
|
||||
@@ -14,6 +14,7 @@ const { erase, cursor } = require('sisteransi');
|
||||
* @param {Number} [opts.initial] Index of default value
|
||||
* @param {Stream} [opts.stdin] The Readable stream to listen to
|
||||
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
|
||||
* @param {Number} [opts.optionsPerPage=10] Max options to display at once
|
||||
*/
|
||||
class SelectPrompt extends Prompt {
|
||||
constructor(opts={}) {
|
||||
@@ -22,16 +23,18 @@ class SelectPrompt extends Prompt {
|
||||
this.hint = opts.hint || '- Use arrow-keys. Return to submit.';
|
||||
this.warn = opts.warn || '- This option is disabled';
|
||||
this.cursor = opts.initial || 0;
|
||||
this.choices = opts.choices.map((ch, idx) => {
|
||||
this.choices = opts.choices.map((ch, idx) => {
|
||||
if (typeof ch === 'string')
|
||||
ch = {title: ch, value: idx};
|
||||
return {
|
||||
title: ch && (ch.title || ch.value || ch),
|
||||
value: ch && (ch.value || idx),
|
||||
value: ch && (ch.value === undefined ? idx : ch.value),
|
||||
description: ch && ch.description,
|
||||
selected: ch && ch.selected,
|
||||
disabled: ch && ch.disabled
|
||||
};
|
||||
});
|
||||
this.optionsPerPage = opts.optionsPerPage || 10;
|
||||
this.value = (this.choices[this.cursor] || {}).value;
|
||||
this.clear = clear('');
|
||||
this.render();
|
||||
@@ -107,37 +110,55 @@ class SelectPrompt extends Prompt {
|
||||
render() {
|
||||
if (this.closed) return;
|
||||
if (this.firstRender) this.out.write(cursor.hide);
|
||||
else this.out.write(erase.lines(this.choices.length + 1));
|
||||
else this.out.write(clear(this.outputText));
|
||||
super.render();
|
||||
|
||||
let { startIndex, endIndex } = entriesToDisplay(this.cursor, this.choices.length, this.optionsPerPage);
|
||||
|
||||
// Print prompt
|
||||
this.out.write([
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(false),
|
||||
this.done ? this.selection.title : this.selection.disabled
|
||||
? color.yellow(this.warn) : color.gray(this.hint)
|
||||
].join(' '));
|
||||
this.outputText = [
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(false),
|
||||
this.done ? this.selection.title : this.selection.disabled
|
||||
? color.yellow(this.warn) : color.gray(this.hint)
|
||||
].join(' ');
|
||||
|
||||
// Print choices
|
||||
if (!this.done) {
|
||||
this.out.write(
|
||||
'\n' +
|
||||
this.choices
|
||||
.map((v, i) => {
|
||||
let title, prefix;
|
||||
if (v.disabled) {
|
||||
title = this.cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
||||
prefix = this.cursor === i ? color.bold().gray(figures.pointer) + ' ' : ' ';
|
||||
} else {
|
||||
title = this.cursor === i ? color.cyan().underline(v.title) : v.title;
|
||||
prefix = this.cursor === i ? color.cyan(figures.pointer) + ' ' : ' ';
|
||||
}
|
||||
return `${prefix} ${title}`;
|
||||
})
|
||||
.join('\n')
|
||||
);
|
||||
this.outputText += '\n';
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
let title, prefix, desc = '', v = this.choices[i];
|
||||
|
||||
// Determine whether to display "more choices" indicators
|
||||
if (i === startIndex && startIndex > 0) {
|
||||
prefix = figures.arrowUp;
|
||||
} else if (i === endIndex - 1 && endIndex < this.choices.length) {
|
||||
prefix = figures.arrowDown;
|
||||
} else {
|
||||
prefix = ' ';
|
||||
}
|
||||
|
||||
if (v.disabled) {
|
||||
title = this.cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
||||
prefix = (this.cursor === i ? color.bold().gray(figures.pointer) + ' ' : ' ') + prefix;
|
||||
} else {
|
||||
title = this.cursor === i ? color.cyan().underline(v.title) : v.title;
|
||||
prefix = (this.cursor === i ? color.cyan(figures.pointer) + ' ' : ' ') + prefix;
|
||||
if (v.description && this.cursor === i) {
|
||||
desc = ` - ${v.description}`;
|
||||
if (prefix.length + title.length + desc.length >= this.out.columns
|
||||
|| v.description.split(/\r?\n/).length > 1) {
|
||||
desc = '\n' + wrap(v.description, { margin: 3, width: this.out.columns });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.outputText += `${prefix} ${title}${color.gray(desc)}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
this.out.write(this.outputText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
node_modules/prompts/lib/elements/text.js
generated
vendored
33
node_modules/prompts/lib/elements/text.js
generated
vendored
@@ -1,7 +1,7 @@
|
||||
const color = require('kleur');
|
||||
const Prompt = require('./prompt');
|
||||
const { cursor } = require('sisteransi');
|
||||
const { style, clear, strip, figures } = require('../util');
|
||||
const { erase, cursor } = require('sisteransi');
|
||||
const { style, clear, lines, figures } = require('../util');
|
||||
|
||||
/**
|
||||
* TextPrompt Base Element
|
||||
@@ -154,36 +154,27 @@ class TextPrompt extends Prompt {
|
||||
|
||||
render() {
|
||||
if (this.closed) return;
|
||||
if (!this.firstRender) {
|
||||
if (this.outputError)
|
||||
this.out.write(cursor.down(lines(this.outputError) - 1) + clear(this.outputError));
|
||||
this.out.write(clear(this.outputText));
|
||||
}
|
||||
super.render();
|
||||
let erase = (this.lines ? cursor.down(this.lines) : ``)+this.clear;
|
||||
this.lines = 0;
|
||||
this.outputError = '';
|
||||
|
||||
let prompt = [
|
||||
this.outputText = [
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(this.done),
|
||||
this.red ? color.red(this.rendered) : this.rendered
|
||||
].join(` `);
|
||||
|
||||
let error = ``;
|
||||
if (this.error) {
|
||||
let lines = this.errorMsg.split(`\n`);
|
||||
error += lines.reduce((a, l, i) => a += `\n${i ? ' ' : figures.pointerSmall} ${color.red().italic(l)}`, ``);
|
||||
this.lines = lines.length;
|
||||
this.outputError += this.errorMsg.split(`\n`)
|
||||
.reduce((a, l, i) => a + `\n${i ? ' ' : figures.pointerSmall} ${color.red().italic(l)}`, ``);
|
||||
}
|
||||
|
||||
let position = ``;
|
||||
if (this.lines) {
|
||||
position += cursor.up(this.lines);
|
||||
position += cursor.left+cursor.to(strip(prompt).length);
|
||||
}
|
||||
position += cursor.move(this.placeholder ?
|
||||
-this.initial.length*this.scale :
|
||||
-this.rendered.length+this.cursor*this.scale
|
||||
);
|
||||
|
||||
this.out.write(erase+prompt+error+position);
|
||||
this.clear = clear(prompt+error);
|
||||
this.out.write(erase.line + cursor.to(0) + this.outputText + cursor.save + this.outputError + cursor.restore);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
node_modules/prompts/lib/elements/toggle.js
generated
vendored
22
node_modules/prompts/lib/elements/toggle.js
generated
vendored
@@ -95,19 +95,19 @@ class TogglePrompt extends Prompt {
|
||||
render() {
|
||||
if (this.closed) return;
|
||||
if (this.firstRender) this.out.write(cursor.hide);
|
||||
else this.out.write(clear(this.outputText));
|
||||
super.render();
|
||||
|
||||
this.out.write(
|
||||
erase.lines(this.first ? 1 : this.msg.split(/\n/g).length) +
|
||||
cursor.to(0) + [
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(this.done),
|
||||
this.value ? this.inactive : color.cyan().underline(this.inactive),
|
||||
color.gray('/'),
|
||||
this.value ? color.cyan().underline(this.active) : this.active
|
||||
].join(' ')
|
||||
);
|
||||
this.outputText = [
|
||||
style.symbol(this.done, this.aborted),
|
||||
color.bold(this.msg),
|
||||
style.delimiter(this.done),
|
||||
this.value ? this.inactive : color.cyan().underline(this.inactive),
|
||||
color.gray('/'),
|
||||
this.value ? color.cyan().underline(this.active) : this.active
|
||||
].join(' ');
|
||||
|
||||
this.out.write(erase.line + cursor.to(0) + this.outputText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
node_modules/prompts/lib/index.js
generated
vendored
18
node_modules/prompts/lib/index.js
generated
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
const prompts = require('./prompts');
|
||||
|
||||
const passOn = ['suggest', 'format', 'onState', 'validate', 'onRender'];
|
||||
const passOn = ['suggest', 'format', 'onState', 'validate', 'onRender', 'type'];
|
||||
const noop = () => {};
|
||||
|
||||
/**
|
||||
@@ -16,7 +16,7 @@ async function prompt(questions=[], { onSubmit=noop, onCancel=noop }={}) {
|
||||
const answers = {};
|
||||
const override = prompt._override || {};
|
||||
questions = [].concat(questions);
|
||||
let answer, question, quit, name, type;
|
||||
let answer, question, quit, name, type, lastPrompt;
|
||||
|
||||
const getFormattedAnswer = async (question, answer, skipValidation = false) => {
|
||||
if (!skipValidation && question.validate && question.validate(answer) !== true) {
|
||||
@@ -28,13 +28,22 @@ async function prompt(questions=[], { onSubmit=noop, onCancel=noop }={}) {
|
||||
for (question of questions) {
|
||||
({ name, type } = question);
|
||||
|
||||
// evaluate type first and skip if type is a falsy value
|
||||
if (typeof type === 'function') {
|
||||
type = await type(answer, { ...answers }, question)
|
||||
question['type'] = type
|
||||
}
|
||||
if (!type) continue;
|
||||
|
||||
// if property is a function, invoke it unless it's a special function
|
||||
for (let key in question) {
|
||||
if (passOn.includes(key)) continue;
|
||||
let value = question[key];
|
||||
question[key] = typeof value === 'function' ? await value(answer, { ...answers }, question) : value;
|
||||
question[key] = typeof value === 'function' ? await value(answer, { ...answers }, lastPrompt) : value;
|
||||
}
|
||||
|
||||
lastPrompt = question;
|
||||
|
||||
if (typeof question.message !== 'string') {
|
||||
throw new Error('prompt message is required');
|
||||
}
|
||||
@@ -42,9 +51,6 @@ async function prompt(questions=[], { onSubmit=noop, onCancel=noop }={}) {
|
||||
// update vars in case they changed
|
||||
({ name, type } = question);
|
||||
|
||||
// skip if type is a falsy value
|
||||
if (!type) continue;
|
||||
|
||||
if (prompts[type] === void 0) {
|
||||
throw new Error(`prompt type (${type}) is not defined`);
|
||||
}
|
||||
|
||||
9
node_modules/prompts/lib/util/action.js
generated
vendored
9
node_modules/prompts/lib/util/action.js
generated
vendored
@@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = key => {
|
||||
module.exports = (key, isSelect) => {
|
||||
if (key.meta) return;
|
||||
|
||||
if (key.ctrl) {
|
||||
if (key.name === 'a') return 'first';
|
||||
if (key.name === 'c') return 'abort';
|
||||
@@ -8,6 +10,11 @@ module.exports = key => {
|
||||
if (key.name === 'e') return 'last';
|
||||
if (key.name === 'g') return 'reset';
|
||||
}
|
||||
|
||||
if (isSelect) {
|
||||
if (key.name === 'j') return 'down';
|
||||
if (key.name === 'k') return 'up';
|
||||
}
|
||||
|
||||
if (key.name === 'return') return 'submit';
|
||||
if (key.name === 'enter') return 'submit'; // ctrl + J
|
||||
|
||||
21
node_modules/prompts/lib/util/entriesToDisplay.js
generated
vendored
Normal file
21
node_modules/prompts/lib/util/entriesToDisplay.js
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Determine what entries should be displayed on the screen, based on the
|
||||
* currently selected index and the maximum visible. Used in list-based
|
||||
* prompts like `select` and `multiselect`.
|
||||
*
|
||||
* @param {number} cursor the currently selected entry
|
||||
* @param {number} total the total entries available to display
|
||||
* @param {number} [maxVisible] the number of entries that can be displayed
|
||||
*/
|
||||
module.exports = (cursor, total, maxVisible) => {
|
||||
maxVisible = maxVisible || total;
|
||||
|
||||
let startIndex = Math.min(total- maxVisible, cursor - Math.floor(maxVisible / 2));
|
||||
if (startIndex < 0) startIndex = 0;
|
||||
|
||||
let endIndex = Math.min(startIndex + maxVisible, total);
|
||||
|
||||
return { startIndex, endIndex };
|
||||
};
|
||||
5
node_modules/prompts/lib/util/index.js
generated
vendored
5
node_modules/prompts/lib/util/index.js
generated
vendored
@@ -5,5 +5,8 @@ module.exports = {
|
||||
clear: require('./clear'),
|
||||
style: require('./style'),
|
||||
strip: require('./strip'),
|
||||
figures: require('./figures')
|
||||
figures: require('./figures'),
|
||||
lines: require('./lines'),
|
||||
wrap: require('./wrap'),
|
||||
entriesToDisplay: require('./entriesToDisplay')
|
||||
};
|
||||
|
||||
11
node_modules/prompts/lib/util/lines.js
generated
vendored
Normal file
11
node_modules/prompts/lib/util/lines.js
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const strip = require('./strip');
|
||||
|
||||
module.exports = function (msg, perLine = process.stdout.columns) {
|
||||
let lines = String(strip(msg) || '').split(/\r?\n/);
|
||||
|
||||
if (!perLine) return lines.length;
|
||||
return lines.map(l => Math.ceil(l.length / perLine))
|
||||
.reduce((a, b) => a + b);
|
||||
};
|
||||
27
node_modules/prompts/lib/util/wrap.js
generated
vendored
Normal file
27
node_modules/prompts/lib/util/wrap.js
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @param {string} msg The message to wrap
|
||||
* @param {object} [opts]
|
||||
* @param {number|string} [opts.margin] Left margin
|
||||
* @param {number} [opts.width] Maximum characters per line including the margin
|
||||
*/
|
||||
module.exports = (msg, opts = {}) => {
|
||||
const tab = Number.isSafeInteger(parseInt(opts.margin))
|
||||
? new Array(parseInt(opts.margin)).fill(' ').join('')
|
||||
: (opts.margin || '');
|
||||
|
||||
const width = opts.width || process.stdout.columns;
|
||||
|
||||
return (msg || '').split(/\r?\n/g)
|
||||
.map(line => line
|
||||
.split(/\s+/g)
|
||||
.reduce((arr, w) => {
|
||||
if (w.length + tab.length >= width || arr[arr.length - 1].length + w.length + 1 < width)
|
||||
arr[arr.length - 1] += ` ${w}`;
|
||||
else arr.push(`${tab}${w}`);
|
||||
return arr;
|
||||
}, [ tab ])
|
||||
.join('\n'))
|
||||
.join('\n');
|
||||
};
|
||||
Reference in New Issue
Block a user