const stringWidth = require('string-width'); function codeRegex(capture) { return capture ? /\u001b\[((?:\d*;){0,5}\d*)m/g : /\u001b\[(?:\d*;){0,5}\d*m/g; } function strlen(str) { let code = codeRegex(); let stripped = ('' + str).replace(code, ''); let split = stripped.split('\n'); return split.reduce(function (memo, s) { return stringWidth(s) > memo ? stringWidth(s) : memo; }, 0); } function repeat(str, times) { return Array(times + 1).join(str); } function pad(str, len, pad, dir) { let length = strlen(str); if (len + 1 >= length) { let padlen = len - length; switch (dir) { case 'right': { str = repeat(pad, padlen) + str; break; } case 'center': { let right = Math.ceil(padlen / 2); let left = padlen - right; str = repeat(pad, left) + str + repeat(pad, right); break; } default: { str = str + repeat(pad, padlen); break; } } } return str; } let codeCache = {}; function addToCodeCache(name, on, off) { on = '\u001b[' + on + 'm'; off = '\u001b[' + off + 'm'; codeCache[on] = { set: name, to: true }; codeCache[off] = { set: name, to: false }; codeCache[name] = { on: on, off: off }; } //https://github.com/Marak/colors.js/blob/master/lib/styles.js addToCodeCache('bold', 1, 22); addToCodeCache('italics', 3, 23); addToCodeCache('underline', 4, 24); addToCodeCache('inverse', 7, 27); addToCodeCache('strikethrough', 9, 29); function updateState(state, controlChars) { let controlCode = controlChars[1] ? parseInt(controlChars[1].split(';')[0]) : 0; if ((controlCode >= 30 && controlCode <= 39) || (controlCode >= 90 && controlCode <= 97)) { state.lastForegroundAdded = controlChars[0]; return; } if ((controlCode >= 40 && controlCode <= 49) || (controlCode >= 100 && controlCode <= 107)) { state.lastBackgroundAdded = controlChars[0]; return; } if (controlCode === 0) { for (let i in state) { /* istanbul ignore else */ if (Object.prototype.hasOwnProperty.call(state, i)) { delete state[i]; } } return; } let info = codeCache[controlChars[0]]; if (info) { state[info.set] = info.to; } } function readState(line) { let code = codeRegex(true); let controlChars = code.exec(line); let state = {}; while (controlChars !== null) { updateState(state, controlChars); controlChars = code.exec(line); } return state; } function unwindState(state, ret) { let lastBackgroundAdded = state.lastBackgroundAdded; let lastForegroundAdded = state.lastForegroundAdded; delete state.lastBackgroundAdded; delete state.lastForegroundAdded; Object.keys(state).forEach(function (key) { if (state[key]) { ret += codeCache[key].off; } }); if (lastBackgroundAdded && lastBackgroundAdded != '\u001b[49m') { ret += '\u001b[49m'; } if (lastForegroundAdded && lastForegroundAdded != '\u001b[39m') { ret += '\u001b[39m'; } return ret; } function rewindState(state, ret) { let lastBackgroundAdded = state.lastBackgroundAdded; let lastForegroundAdded = state.lastForegroundAdded; delete state.lastBackgroundAdded; delete state.lastForegroundAdded; Object.keys(state).forEach(function (key) { if (state[key]) { ret = codeCache[key].on + ret; } }); if (lastBackgroundAdded && lastBackgroundAdded != '\u001b[49m') { ret = lastBackgroundAdded + ret; } if (lastForegroundAdded && lastForegroundAdded != '\u001b[39m') { ret = lastForegroundAdded + ret; } return ret; } function truncateWidth(str, desiredLength) { if (str.length === strlen(str)) { return str.substr(0, desiredLength); } while (strlen(str) > desiredLength) { str = str.slice(0, -1); } return str; } function truncateWidthWithAnsi(str, desiredLength) { let code = codeRegex(true); let split = str.split(codeRegex()); let splitIndex = 0; let retLen = 0; let ret = ''; let myArray; let state = {}; while (retLen < desiredLength) { myArray = code.exec(str); let toAdd = split[splitIndex]; splitIndex++; if (retLen + strlen(toAdd) > desiredLength) { toAdd = truncateWidth(toAdd, desiredLength - retLen); } ret += toAdd; retLen += strlen(toAdd); if (retLen < desiredLength) { if (!myArray) { break; } // full-width chars may cause a whitespace which cannot be filled ret += myArray[0]; updateState(state, myArray); } } return unwindState(state, ret); } function truncate(str, desiredLength, truncateChar) { truncateChar = truncateChar || '…'; let lengthOfStr = strlen(str); if (lengthOfStr <= desiredLength) { return str; } desiredLength -= strlen(truncateChar); let ret = truncateWidthWithAnsi(str, desiredLength); return ret + truncateChar; } function defaultOptions() { return { chars: { top: '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐', bottom: '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘', left: '│', 'left-mid': '├', mid: '─', 'mid-mid': '┼', right: '│', 'right-mid': '┤', middle: '│', }, truncate: '…', colWidths: [], rowHeights: [], colAligns: [], rowAligns: [], style: { 'padding-left': 1, 'padding-right': 1, head: ['red'], border: ['grey'], compact: false, }, head: [], }; } function mergeOptions(options, defaults) { options = options || {}; defaults = defaults || defaultOptions(); let ret = Object.assign({}, defaults, options); ret.chars = Object.assign({}, defaults.chars, options.chars); ret.style = Object.assign({}, defaults.style, options.style); return ret; } // Wrap on word boundary function wordWrap(maxLength, input) { let lines = []; let split = input.split(/(\s+)/g); let line = []; let lineLength = 0; let whitespace; for (let i = 0; i < split.length; i += 2) { let word = split[i]; let newLength = lineLength + strlen(word); if (lineLength > 0 && whitespace) { newLength += whitespace.length; } if (newLength > maxLength) { if (lineLength !== 0) { lines.push(line.join('')); } line = [word]; lineLength = strlen(word); } else { line.push(whitespace || '', word); lineLength = newLength; } whitespace = split[i + 1]; } if (lineLength) { lines.push(line.join('')); } return lines; } // Wrap text (ignoring word boundaries) function textWrap(maxLength, input) { let lines = []; let line = ''; function pushLine(str, ws) { if (line.length && ws) line += ws; line += str; while (line.length > maxLength) { lines.push(line.slice(0, maxLength)); line = line.slice(maxLength); } } let split = input.split(/(\s+)/g); for (let i = 0; i < split.length; i += 2) { pushLine(split[i], i && split[i - 1]); } if (line.length) lines.push(line); return lines; } function multiLineWordWrap(maxLength, input, wrapOnWordBoundary = true) { let output = []; input = input.split('\n'); const handler = wrapOnWordBoundary ? wordWrap : textWrap; for (let i = 0; i < input.length; i++) { output.push.apply(output, handler(maxLength, input[i])); } return output; } function colorizeLines(input) { let state = {}; let output = []; for (let i = 0; i < input.length; i++) { let line = rewindState(state, input[i]); state = readState(line); let temp = Object.assign({}, state); output.push(unwindState(temp, line)); } return output; } /** * Credit: Matheus Sampaio https://github.com/matheussampaio */ function hyperlink(url, text) { const OSC = '\u001B]'; const BEL = '\u0007'; const SEP = ';'; return [OSC, '8', SEP, SEP, url || text, BEL, text, OSC, '8', SEP, SEP, BEL].join(''); } module.exports = { strlen: strlen, repeat: repeat, pad: pad, truncate: truncate, mergeOptions: mergeOptions, wordWrap: multiLineWordWrap, colorizeLines: colorizeLines, hyperlink, };