shelobsay
Version:
Tell Shelob what to say
176 lines (143 loc) • 5.95 kB
JavaScript
;
const chalk = require('chalk');
const pad = require('pad-component');
const wrap = require('wrap-ansi');
const stringWidth = require('string-width');
const stripAnsi = require('strip-ansi');
const ansiStyles = require('ansi-styles');
const ansiRegex = require('ansi-regex')();
const cliBoxes = require('cli-boxes');
const border = cliBoxes.round;
const leftOffset = 22;
const defaultGreeting = '\n' +
chalk.gray(' \\_______/ ') + '\n' +
chalk.gray(' `.,-\'\\_____/`-.,\' ') + '\n' +
chalk.gray(' /`..\'\\ _ /`.,\'\\ ') + '\n' +
chalk.gray(' / /`.,\' `.,\'\\ \\ ') + '\n' +
chalk.gray('/__/__/ \\__\\__\\__ ') + '\n' +
chalk.gray('\\ \\ \\ / / / ') + '\n' +
chalk.gray(' \\ \\,\'`._,\'`./ / ') + '\n' +
chalk.gray(' \\,\'`./___\\,\'`./ ') + '\n' +
chalk.gray(' ,\'`-./_____\\,-\'`. ') + '\n' +
chalk.gray(' / \\ ');
module.exports = (message, options) => {
message = (message || 'Welcome to Shelob!').trim();
options = options || {};
/*
* What you're about to see may confuse you. And rightfully so. Here's an
* explanation.
*
* When yosay is given a string, we create a duplicate with the ansi styling
* sucked out. This way, the true length of the string is read by `pad` and
* `wrap`, so they can correctly do their job without getting tripped up by
* the "invisible" ansi. Along with the duplicated, non-ansi string, we store
* the character position of where the ansi was, so that when we go back over
* each line that will be printed out in the message box, we check the
* character position to see if it needs any styling, then re-insert it if
* necessary.
*
* Better implementations welcome :)
*/
let maxLength = 24;
const styledIndexes = {};
let completedString = '';
let topOffset = 4;
// Amount of characters of the yeoman character »column« → ` /___A___\ /`
const YEOMAN_CHARACTER_WIDTH = 22;
// Amount of characters of the default top frame of the speech bubble → `╭──────────────────────────╮`
const DEFAULT_TOP_FRAME_WIDTH = 28;
// Amount of characters of a total line
let TOTAL_CHARACTERS_PER_LINE = YEOMAN_CHARACTER_WIDTH + DEFAULT_TOP_FRAME_WIDTH;
// The speech bubble will overflow the Yeoman character if the message is too long.
const MAX_MESSAGE_LINES_BEFORE_OVERFLOW = 11;
if (options.maxLength) {
maxLength = stripAnsi(message).toLowerCase().split(' ').sort()[0].length;
if (maxLength < options.maxLength) {
maxLength = options.maxLength;
TOTAL_CHARACTERS_PER_LINE = maxLength + YEOMAN_CHARACTER_WIDTH + topOffset;
}
}
const regExNewLine = new RegExp(`\\s{${maxLength}}`);
const borderHorizontal = border.horizontal.repeat(maxLength + 2);
const frame = {
top: border.topLeft + borderHorizontal + border.topRight,
side: ansiStyles.reset.open + border.vertical + ansiStyles.reset.open,
bottom: ansiStyles.reset.open + border.bottomLeft + borderHorizontal + border.bottomRight
};
message.replace(ansiRegex, (match, offset) => {
Object.keys(styledIndexes).forEach(key => {
offset -= styledIndexes[key].length;
});
styledIndexes[offset] = styledIndexes[offset] ? styledIndexes[offset] + match : match;
});
return wrap(stripAnsi(message), maxLength, {hard: true})
.split(/\n/)
.reduce((greeting, str, index, array) => {
if (!regExNewLine.test(str)) {
str = str.trim();
}
completedString += str;
str = completedString
.substr(completedString.length - str.length)
.replace(/./g, (char, charIndex) => {
if (index > 0) {
charIndex += completedString.length - str.length + index;
}
let hasContinuedStyle = 0;
let continuedStyle;
Object.keys(styledIndexes).forEach(offset => {
if (charIndex > offset) {
hasContinuedStyle++;
continuedStyle = styledIndexes[offset];
}
if (hasContinuedStyle === 1 && charIndex < offset) {
hasContinuedStyle++;
}
});
if (styledIndexes[charIndex]) {
return styledIndexes[charIndex] + char;
} else if (hasContinuedStyle >= 2) {
return continuedStyle + char;
}
return char;
})
.trim();
const paddedString = pad({
length: stringWidth(str),
valueOf() {
return ansiStyles.reset.open + str + ansiStyles.reset.open;
}
}, maxLength);
if (index === 0) {
// Need to adjust the top position of the speech bubble depending on the
// amount of lines of the message.
if (array.length === 2) {
topOffset -= 1;
}
if (array.length >= 3) {
topOffset -= 2;
}
// The speech bubble will overflow the Yeoman character if the message
// is too long. So we vertically center the bubble by adding empty lines
// on top of the greeting.
if (array.length > MAX_MESSAGE_LINES_BEFORE_OVERFLOW) {
const emptyLines = Math.ceil((array.length - MAX_MESSAGE_LINES_BEFORE_OVERFLOW) / 2);
for (let i = 0; i < emptyLines; i++) {
greeting.unshift('');
}
frame.top = pad.left(frame.top, TOTAL_CHARACTERS_PER_LINE);
}
greeting[topOffset - 1] += frame.top;
}
greeting[index + topOffset] =
(greeting[index + topOffset] || pad.left('', leftOffset)) +
frame.side + ' ' + paddedString + ' ' + frame.side;
if (array.length === index + 1) {
greeting[index + topOffset + 1] =
(greeting[index + topOffset + 1] || pad.left('', leftOffset)) +
frame.bottom;
}
return greeting;
}, defaultGreeting.split(/\n/))
.join('\n') + '\n';
};