'use strict'; const align = { right: alignRight, center: alignCenter }; const top = 0; const right = 1; const bottom = 2; const left = 3; class UI { constructor(opts) { var _a; this.width = opts.width; this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true; this.rows = []; } span(...args) { const cols = this.div(...args); cols.span = true; } resetOutput() { this.rows = []; } div(...args) { if (args.length === 0) { this.div(''); } if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') { return this.applyLayoutDSL(args[0]); } const cols = args.map(arg => { if (typeof arg === 'string') { return this.colFromString(arg); } return arg; }); this.rows.push(cols); return cols; } shouldApplyLayoutDSL(...args) { return args.length === 1 && typeof args[0] === 'string' && /[\t\n]/.test(args[0]); } applyLayoutDSL(str) { const rows = str.split('\n').map(row => row.split('\t')); let leftColumnWidth = 0; // simple heuristic for layout, make sure the // second column lines up along the left-hand. // don't allow the first column to take up more // than 50% of the screen. rows.forEach(columns => { if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) { leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0])); } }); // generate a table: // replacing ' ' with padding calculations. // using the algorithmically generated width. rows.forEach(columns => { this.div(...columns.map((r, i) => { return { text: r.trim(), padding: this.measurePadding(r), width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined }; })); }); return this.rows[this.rows.length - 1]; } colFromString(text) { return { text, padding: this.measurePadding(text) }; } measurePadding(str) { // measure padding without ansi escape codes const noAnsi = mixin.stripAnsi(str); return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length]; } toString() { const lines = []; this.rows.forEach(row => { this.rowToString(row, lines); }); // don't display any lines with the // hidden flag set. return lines .filter(line => !line.hidden) .map(line => line.text) .join('\n'); } rowToString(row, lines) { this.rasterize(row).forEach((rrow, r) => { let str = ''; rrow.forEach((col, c) => { const { width } = row[c]; // the width with padding. const wrapWidth = this.negatePadding(row[c]); // the width without padding. let ts = col; // temporary string used during alignment/padding. if (wrapWidth > mixin.stringWidth(col)) { ts += ' '.repeat(wrapWidth - mixin.stringWidth(col)); } // align the string within its column. if (row[c].align && row[c].align !== 'left' && this.wrap) { const fn = align[row[c].align]; ts = fn(ts, wrapWidth); if (mixin.stringWidth(ts) < wrapWidth) { ts += ' '.repeat((width || 0) - mixin.stringWidth(ts) - 1); } } // apply border and padding to string. const padding = row[c].padding || [0, 0, 0, 0]; if (padding[left]) { str += ' '.repeat(padding[left]); } str += addBorder(row[c], ts, '| '); str += ts; str += addBorder(row[c], ts, ' |'); if (padding[right]) { str += ' '.repeat(padding[right]); } // if prior row is span, try to render the // current row on the prior line. if (r === 0 && lines.length > 0) { str = this.renderInline(str, lines[lines.length - 1]); } }); // remove trailing whitespace. lines.push({ text: str.replace(/ +$/, ''), span: row.span }); }); return lines; } // if the full 'source' can render in // the target line, do so. renderInline(source, previousLine) { const match = source.match(/^ */); const leadingWhitespace = match ? match[0].length : 0; const target = previousLine.text; const targetTextWidth = mixin.stringWidth(target.trimRight()); if (!previousLine.span) { return source; } // if we're not applying wrapping logic, // just always append to the span. if (!this.wrap) { previousLine.hidden = true; return target + source; } if (leadingWhitespace < targetTextWidth) { return source; } previousLine.hidden = true; return target.trimRight() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft(); } rasterize(row) { const rrows = []; const widths = this.columnWidths(row); let wrapped; // word wrap all columns, and create // a data-structure that is easy to rasterize. row.forEach((col, c) => { // leave room for left and right padding. col.width = widths[c]; if (this.wrap) { wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n'); } else { wrapped = col.text.split('\n'); } if (col.border) { wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.'); wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'"); } // add top and bottom padding. if (col.padding) { wrapped.unshift(...new Array(col.padding[top] || 0).fill('')); wrapped.push(...new Array(col.padding[bottom] || 0).fill('')); } wrapped.forEach((str, r) => { if (!rrows[r]) { rrows.push([]); } const rrow = rrows[r]; for (let i = 0; i < c; i++) { if (rrow[i] === undefined) { rrow.push(''); } } rrow.push(str); }); }); return rrows; } negatePadding(col) { let wrapWidth = col.width || 0; if (col.padding) { wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0); } if (col.border) { wrapWidth -= 4; } return wrapWidth; } columnWidths(row) { if (!this.wrap) { return row.map(col => { return col.width || mixin.stringWidth(col.text); }); } let unset = row.length; let remainingWidth = this.width; // column widths can be set in config. const widths = row.map(col => { if (col.width) { unset--; remainingWidth -= col.width; return col.width; } return undefined; }); // any unset widths should be calculated. const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0; return widths.map((w, i) => { if (w === undefined) { return Math.max(unsetWidth, _minWidth(row[i])); } return w; }); } } function addBorder(col, ts, style) { if (col.border) { if (/[.']-+[.']/.test(ts)) { return ''; } if (ts.trim().length !== 0) { return style; } return ' '; } return ''; } // calculates the minimum width of // a column, based on padding preferences. function _minWidth(col) { const padding = col.padding || []; const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0); if (col.border) { return minWidth + 4; } return minWidth; } function getWindowWidth() { /* istanbul ignore next: depends on terminal */ if (typeof process === 'object' && process.stdout && process.stdout.columns) { return process.stdout.columns; } return 80; } function alignRight(str, width) { str = str.trim(); const strWidth = mixin.stringWidth(str); if (strWidth < width) { return ' '.repeat(width - strWidth) + str; } return str; } function alignCenter(str, width) { str = str.trim(); const strWidth = mixin.stringWidth(str); /* istanbul ignore next */ if (strWidth >= width) { return str; } return ' '.repeat((width - strWidth) >> 1) + str; } let mixin; function cliui(opts, _mixin) { mixin = _mixin; return new UI({ width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(), wrap: opts === null || opts === void 0 ? void 0 : opts.wrap }); } // Bootstrap cliui with CommonJS dependencies: const stringWidth = require('string-width'); const stripAnsi = require('strip-ansi'); const wrap = require('wrap-ansi'); function ui(opts) { return cliui(opts, { stringWidth, stripAnsi, wrap }); } module.exports = ui;