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