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