1const stringWidth = require('string-width');
2
3function codeRegex(capture) {
4  return capture ? /\u001b\[((?:\d*;){0,5}\d*)m/g : /\u001b\[(?:\d*;){0,5}\d*m/g;
5}
6
7function strlen(str) {
8  let code = codeRegex();
9  let stripped = ('' + str).replace(code, '');
10  let split = stripped.split('\n');
11  return split.reduce(function (memo, s) {
12    return stringWidth(s) > memo ? stringWidth(s) : memo;
13  }, 0);
14}
15
16function repeat(str, times) {
17  return Array(times + 1).join(str);
18}
19
20function pad(str, len, pad, dir) {
21  let length = strlen(str);
22  if (len + 1 >= length) {
23    let padlen = len - length;
24    switch (dir) {
25      case 'right': {
26        str = repeat(pad, padlen) + str;
27        break;
28      }
29      case 'center': {
30        let right = Math.ceil(padlen / 2);
31        let left = padlen - right;
32        str = repeat(pad, left) + str + repeat(pad, right);
33        break;
34      }
35      default: {
36        str = str + repeat(pad, padlen);
37        break;
38      }
39    }
40  }
41  return str;
42}
43
44let codeCache = {};
45
46function addToCodeCache(name, on, off) {
47  on = '\u001b[' + on + 'm';
48  off = '\u001b[' + off + 'm';
49  codeCache[on] = { set: name, to: true };
50  codeCache[off] = { set: name, to: false };
51  codeCache[name] = { on: on, off: off };
52}
53
54//https://github.com/Marak/colors.js/blob/master/lib/styles.js
55addToCodeCache('bold', 1, 22);
56addToCodeCache('italics', 3, 23);
57addToCodeCache('underline', 4, 24);
58addToCodeCache('inverse', 7, 27);
59addToCodeCache('strikethrough', 9, 29);
60
61function updateState(state, controlChars) {
62  let controlCode = controlChars[1] ? parseInt(controlChars[1].split(';')[0]) : 0;
63  if ((controlCode >= 30 && controlCode <= 39) || (controlCode >= 90 && controlCode <= 97)) {
64    state.lastForegroundAdded = controlChars[0];
65    return;
66  }
67  if ((controlCode >= 40 && controlCode <= 49) || (controlCode >= 100 && controlCode <= 107)) {
68    state.lastBackgroundAdded = controlChars[0];
69    return;
70  }
71  if (controlCode === 0) {
72    for (let i in state) {
73      /* istanbul ignore else */
74      if (Object.prototype.hasOwnProperty.call(state, i)) {
75        delete state[i];
76      }
77    }
78    return;
79  }
80  let info = codeCache[controlChars[0]];
81  if (info) {
82    state[info.set] = info.to;
83  }
84}
85
86function readState(line) {
87  let code = codeRegex(true);
88  let controlChars = code.exec(line);
89  let state = {};
90  while (controlChars !== null) {
91    updateState(state, controlChars);
92    controlChars = code.exec(line);
93  }
94  return state;
95}
96
97function unwindState(state, ret) {
98  let lastBackgroundAdded = state.lastBackgroundAdded;
99  let lastForegroundAdded = state.lastForegroundAdded;
100
101  delete state.lastBackgroundAdded;
102  delete state.lastForegroundAdded;
103
104  Object.keys(state).forEach(function (key) {
105    if (state[key]) {
106      ret += codeCache[key].off;
107    }
108  });
109
110  if (lastBackgroundAdded && lastBackgroundAdded != '\u001b[49m') {
111    ret += '\u001b[49m';
112  }
113  if (lastForegroundAdded && lastForegroundAdded != '\u001b[39m') {
114    ret += '\u001b[39m';
115  }
116
117  return ret;
118}
119
120function rewindState(state, ret) {
121  let lastBackgroundAdded = state.lastBackgroundAdded;
122  let lastForegroundAdded = state.lastForegroundAdded;
123
124  delete state.lastBackgroundAdded;
125  delete state.lastForegroundAdded;
126
127  Object.keys(state).forEach(function (key) {
128    if (state[key]) {
129      ret = codeCache[key].on + ret;
130    }
131  });
132
133  if (lastBackgroundAdded && lastBackgroundAdded != '\u001b[49m') {
134    ret = lastBackgroundAdded + ret;
135  }
136  if (lastForegroundAdded && lastForegroundAdded != '\u001b[39m') {
137    ret = lastForegroundAdded + ret;
138  }
139
140  return ret;
141}
142
143function truncateWidth(str, desiredLength) {
144  if (str.length === strlen(str)) {
145    return str.substr(0, desiredLength);
146  }
147
148  while (strlen(str) > desiredLength) {
149    str = str.slice(0, -1);
150  }
151
152  return str;
153}
154
155function truncateWidthWithAnsi(str, desiredLength) {
156  let code = codeRegex(true);
157  let split = str.split(codeRegex());
158  let splitIndex = 0;
159  let retLen = 0;
160  let ret = '';
161  let myArray;
162  let state = {};
163
164  while (retLen < desiredLength) {
165    myArray = code.exec(str);
166    let toAdd = split[splitIndex];
167    splitIndex++;
168    if (retLen + strlen(toAdd) > desiredLength) {
169      toAdd = truncateWidth(toAdd, desiredLength - retLen);
170    }
171    ret += toAdd;
172    retLen += strlen(toAdd);
173
174    if (retLen < desiredLength) {
175      if (!myArray) {
176        break;
177      } // full-width chars may cause a whitespace which cannot be filled
178      ret += myArray[0];
179      updateState(state, myArray);
180    }
181  }
182
183  return unwindState(state, ret);
184}
185
186function truncate(str, desiredLength, truncateChar) {
187  truncateChar = truncateChar || '…';
188  let lengthOfStr = strlen(str);
189  if (lengthOfStr <= desiredLength) {
190    return str;
191  }
192  desiredLength -= strlen(truncateChar);
193
194  let ret = truncateWidthWithAnsi(str, desiredLength);
195
196  return ret + truncateChar;
197}
198
199function defaultOptions() {
200  return {
201    chars: {
202      top: '─',
203      'top-mid': '┬',
204      'top-left': '┌',
205      'top-right': '┐',
206      bottom: '─',
207      'bottom-mid': '┴',
208      'bottom-left': '└',
209      'bottom-right': '┘',
210      left: '│',
211      'left-mid': '├',
212      mid: '─',
213      'mid-mid': '┼',
214      right: '│',
215      'right-mid': '┤',
216      middle: '│',
217    },
218    truncate: '…',
219    colWidths: [],
220    rowHeights: [],
221    colAligns: [],
222    rowAligns: [],
223    style: {
224      'padding-left': 1,
225      'padding-right': 1,
226      head: ['red'],
227      border: ['grey'],
228      compact: false,
229    },
230    head: [],
231  };
232}
233
234function mergeOptions(options, defaults) {
235  options = options || {};
236  defaults = defaults || defaultOptions();
237  let ret = Object.assign({}, defaults, options);
238  ret.chars = Object.assign({}, defaults.chars, options.chars);
239  ret.style = Object.assign({}, defaults.style, options.style);
240  return ret;
241}
242
243// Wrap on word boundary
244function wordWrap(maxLength, input) {
245  let lines = [];
246  let split = input.split(/(\s+)/g);
247  let line = [];
248  let lineLength = 0;
249  let whitespace;
250  for (let i = 0; i < split.length; i += 2) {
251    let word = split[i];
252    let newLength = lineLength + strlen(word);
253    if (lineLength > 0 && whitespace) {
254      newLength += whitespace.length;
255    }
256    if (newLength > maxLength) {
257      if (lineLength !== 0) {
258        lines.push(line.join(''));
259      }
260      line = [word];
261      lineLength = strlen(word);
262    } else {
263      line.push(whitespace || '', word);
264      lineLength = newLength;
265    }
266    whitespace = split[i + 1];
267  }
268  if (lineLength) {
269    lines.push(line.join(''));
270  }
271  return lines;
272}
273
274// Wrap text (ignoring word boundaries)
275function textWrap(maxLength, input) {
276  let lines = [];
277  let line = '';
278  function pushLine(str, ws) {
279    if (line.length && ws) line += ws;
280    line += str;
281    while (line.length > maxLength) {
282      lines.push(line.slice(0, maxLength));
283      line = line.slice(maxLength);
284    }
285  }
286  let split = input.split(/(\s+)/g);
287  for (let i = 0; i < split.length; i += 2) {
288    pushLine(split[i], i && split[i - 1]);
289  }
290  if (line.length) lines.push(line);
291  return lines;
292}
293
294function multiLineWordWrap(maxLength, input, wrapOnWordBoundary = true) {
295  let output = [];
296  input = input.split('\n');
297  const handler = wrapOnWordBoundary ? wordWrap : textWrap;
298  for (let i = 0; i < input.length; i++) {
299    output.push.apply(output, handler(maxLength, input[i]));
300  }
301  return output;
302}
303
304function colorizeLines(input) {
305  let state = {};
306  let output = [];
307  for (let i = 0; i < input.length; i++) {
308    let line = rewindState(state, input[i]);
309    state = readState(line);
310    let temp = Object.assign({}, state);
311    output.push(unwindState(temp, line));
312  }
313  return output;
314}
315
316/**
317 * Credit: Matheus Sampaio https://github.com/matheussampaio
318 */
319function hyperlink(url, text) {
320  const OSC = '\u001B]';
321  const BEL = '\u0007';
322  const SEP = ';';
323
324  return [OSC, '8', SEP, SEP, url || text, BEL, text, OSC, '8', SEP, SEP, BEL].join('');
325}
326
327module.exports = {
328  strlen: strlen,
329  repeat: repeat,
330  pad: pad,
331  truncate: truncate,
332  mergeOptions: mergeOptions,
333  wordWrap: multiLineWordWrap,
334  colorizeLines: colorizeLines,
335  hyperlink,
336};
337