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