UNPKG

5.91 kBJavaScriptView Raw
1'use strict';
2const chalk = require('chalk');
3const pad = require('pad-component');
4const wrap = require('wrap-ansi');
5const stringWidth = require('string-width');
6const stripAnsi = require('strip-ansi');
7const ansiStyles = require('ansi-styles');
8const ansiRegex = require('ansi-regex')();
9const cliBoxes = require('cli-boxes');
10
11const border = cliBoxes.round;
12const leftOffset = 17;
13const defaultGreeting =
14 '\n _-----_ ' +
15 '\n | | ' +
16 '\n |' + chalk.red('--(o)--') + '| ' +
17 '\n `---------´ ' +
18 '\n ' + chalk.yellow('(') + ' _' + chalk.yellow('´U`') + '_ ' + chalk.yellow(')') + ' ' +
19 '\n /___A___\\ /' +
20 '\n ' + chalk.yellow('| ~ |') + ' ' +
21 '\n __' + chalk.yellow('\'.___.\'') + '__ ' +
22 '\n ´ ' + chalk.red('` |') + '° ' + chalk.red('´ Y') + ' ` ';
23
24module.exports = (message, options) => {
25 message = (message || 'Welcome to Yeoman, ladies and gentlemen!').trim();
26 options = options || {};
27
28 /*
29 * What you're about to see may confuse you. And rightfully so. Here's an
30 * explanation.
31 *
32 * When yosay is given a string, we create a duplicate with the ansi styling
33 * sucked out. This way, the true length of the string is read by `pad` and
34 * `wrap`, so they can correctly do their job without getting tripped up by
35 * the "invisible" ansi. Along with the duplicated, non-ansi string, we store
36 * the character position of where the ansi was, so that when we go back over
37 * each line that will be printed out in the message box, we check the
38 * character position to see if it needs any styling, then re-insert it if
39 * necessary.
40 *
41 * Better implementations welcome :)
42 */
43
44 let maxLength = 24;
45 const styledIndexes = {};
46 let completedString = '';
47 let topOffset = 4;
48
49 // Amount of characters of the yeoman character »column« → ` /___A___\ /`
50 const YEOMAN_CHARACTER_WIDTH = 17;
51
52 // Amount of characters of the default top frame of the speech bubble → `╭──────────────────────────╮`
53 const DEFAULT_TOP_FRAME_WIDTH = 28;
54
55 // Amount of characters of a total line
56 let TOTAL_CHARACTERS_PER_LINE = YEOMAN_CHARACTER_WIDTH + DEFAULT_TOP_FRAME_WIDTH;
57
58 // The speech bubble will overflow the Yeoman character if the message is too long.
59 const MAX_MESSAGE_LINES_BEFORE_OVERFLOW = 7;
60
61 if (options.maxLength) {
62 maxLength = stripAnsi(message).toLowerCase().split(' ').sort()[0].length;
63
64 if (maxLength < options.maxLength) {
65 maxLength = options.maxLength;
66 TOTAL_CHARACTERS_PER_LINE = maxLength + YEOMAN_CHARACTER_WIDTH + topOffset;
67 }
68 }
69
70 const regExNewLine = new RegExp(`\\s{${maxLength}}`);
71 const borderHorizontal = border.horizontal.repeat(maxLength + 2);
72
73 const frame = {
74 top: border.topLeft + borderHorizontal + border.topRight,
75 side: ansiStyles.reset.open + border.vertical + ansiStyles.reset.open,
76 bottom: ansiStyles.reset.open + border.bottomLeft + borderHorizontal + border.bottomRight
77 };
78
79 message.replace(ansiRegex, (match, offset) => {
80 Object.keys(styledIndexes).forEach(key => {
81 offset -= styledIndexes[key].length;
82 });
83
84 styledIndexes[offset] = styledIndexes[offset] ? styledIndexes[offset] + match : match;
85 });
86
87 return wrap(stripAnsi(message), maxLength, {hard: true})
88 .split(/\n/)
89 .reduce((greeting, str, index, array) => {
90 if (!regExNewLine.test(str)) {
91 str = str.trim();
92 }
93
94 completedString += str;
95
96 str = completedString
97 .substr(completedString.length - str.length)
98 .replace(/./g, (char, charIndex) => {
99 if (index > 0) {
100 charIndex += completedString.length - str.length + index;
101 }
102
103 let hasContinuedStyle = 0;
104 let continuedStyle;
105
106 Object.keys(styledIndexes).forEach(offset => {
107 if (charIndex > offset) {
108 hasContinuedStyle++;
109 continuedStyle = styledIndexes[offset];
110 }
111
112 if (hasContinuedStyle === 1 && charIndex < offset) {
113 hasContinuedStyle++;
114 }
115 });
116
117 if (styledIndexes[charIndex]) {
118 return styledIndexes[charIndex] + char;
119 } else if (hasContinuedStyle >= 2) {
120 return continuedStyle + char;
121 }
122
123 return char;
124 })
125 .trim();
126
127 const paddedString = pad({
128 length: stringWidth(str),
129 valueOf() {
130 return ansiStyles.reset.open + str + ansiStyles.reset.open;
131 }
132 }, maxLength);
133
134 if (index === 0) {
135 // Need to adjust the top position of the speech bubble depending on the
136 // amount of lines of the message.
137 if (array.length === 2) {
138 topOffset -= 1;
139 }
140
141 if (array.length >= 3) {
142 topOffset -= 2;
143 }
144
145 // The speech bubble will overflow the Yeoman character if the message
146 // is too long. So we vertically center the bubble by adding empty lines
147 // on top of the greeting.
148 if (array.length > MAX_MESSAGE_LINES_BEFORE_OVERFLOW) {
149 const emptyLines = Math.ceil((array.length - MAX_MESSAGE_LINES_BEFORE_OVERFLOW) / 2);
150
151 for (let i = 0; i < emptyLines; i++) {
152 greeting.unshift('');
153 }
154
155 frame.top = pad.left(frame.top, TOTAL_CHARACTERS_PER_LINE);
156 }
157
158 greeting[topOffset - 1] += frame.top;
159 }
160
161 greeting[index + topOffset] =
162 (greeting[index + topOffset] || pad.left('', leftOffset)) +
163 frame.side + ' ' + paddedString + ' ' + frame.side;
164
165 if (array.length === index + 1) {
166 greeting[index + topOffset + 1] =
167 (greeting[index + topOffset + 1] || pad.left('', leftOffset)) +
168 frame.bottom;
169 }
170
171 return greeting;
172 }, defaultGreeting.split(/\n/))
173 .join('\n') + '\n';
174};