UNPKG

begoo

Version:

Say something with style ;)

256 lines (218 loc) 7.42 kB
'use strict' import chalk from 'chalk' import pad from 'pad-component' import wrap from 'wrap-ansi' import stringWidth from 'string-width' import stripAnsi from 'strip-ansi' import ansiStyles from 'ansi-styles' import ansiRegex from 'ansi-regex' import repeating from 'repeating' import cliBoxes from 'cli-boxes' const border = cliBoxes.round let leftOffset = 15 const DEFAULT_GREETING = '\n |\\_/| ' + '\n / ' + chalk.yellow('@') + ' ' + chalk.yellow('@') + ' \\ ' + '\n ( > º < ) ' + '\n `' + chalk.yellow('>>') + chalk.red('x') + chalk.yellow('<<') + '´ ' + '\n / O \\ ' const AVATAR_LIST = [{ name: 'cat', layout: '\n |\\_/| ' + '\n / ' + chalk.yellow('@') + ' ' + chalk.yellow('@') + ' \\ ' + '\n ( > º < ) ' + '\n `' + chalk.yellow('>>') + chalk.red('x') + chalk.yellow('<<') + '´ ' + '\n / O \\ ', width: 15, leftOffset: 15 }, { name: 'dog', layout: '\n __ ' + '\n / \\ ' + '\n / ..|\\ ' + '\n (_\\ |_) ' + '\n / \\@\' ' + '\n / \\ ' + '\n _ / ` | ' + '\n \\\\/ \\ | _\\ ' + '\n \\ /_ || \\\\_ ' + '\n \\____)|_) \\_) ', width: 22, leftOffset: 15 }, { name: 'chicken', layout: '\n \\\\ ' + '\n (o> ' + '\n \\\\_//) ' + '\n \\_/_) ' + '\n _|_ ', width: 16, leftOffset: 13 }, { name: 'monkey', layout: '\n /~\\ ' + '\n C oo ' + '\n _( ^) ' + '\n / ~\\ ', width: 16, leftOffset: 15 }, { name: 'tux', layout: '\n .--. ' + '\n |o_o | ' + '\n |:_/ | ' + '\n // \\ \\ ' + '\n (| | ) ' + '\n /\'\\_ _/`\\ ' + '\n \\___)=(___/ ', width: 16, leftOffset: 15 } ] export function begoo (message, options) { message = (message || 'Welcome to Begoo! \n Meow ...').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 let avatar = DEFAULT_GREETING let frame = { top: '', side: '', bottom: '' } const styledIndexes = {} let completedString = '' let regExNewLine = '' let topOffset = 4 const DEFAULT_CHARACTER_WIDTH = 15 // Amount of characters of the default top frame of the speech bubble → `╭──────────────────────────╮` const DEFAULT_TOP_FRAME_WIDTH = 20 // Amount of characters of a total line let TOTAL_CHARACTERS_PER_LINE = DEFAULT_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 = 7 if (options.maxLength) { maxLength = stripAnsi(message).toLowerCase().split(' ').sort()[0].length if (maxLength < options.maxLength) { maxLength = options.maxLength TOTAL_CHARACTERS_PER_LINE = maxLength + DEFAULT_CHARACTER_WIDTH + topOffset } } if (options.avatar) { avatar = stripAnsi(message).toLowerCase().split(' ').sort()[0].length avatar = AVATAR_LIST.find(avatar => avatar.name === options.avatar) if (typeof avatar === 'undefined') { avatar = DEFAULT_GREETING } else { leftOffset = avatar.leftOffset avatar = avatar.layout TOTAL_CHARACTERS_PER_LINE = avatar.width + DEFAULT_TOP_FRAME_WIDTH } } regExNewLine = new RegExp('\\s{' + maxLength + '}') const borderHorizontal = repeating(maxLength + 2, border.horizontal) 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) => { let paddedString = '' 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() 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 }, avatar.split(/\n/)) .join('\n') + '\n' }