• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const align = {
4    right: alignRight,
5    center: alignCenter
6};
7const top = 0;
8const right = 1;
9const bottom = 2;
10const left = 3;
11class UI {
12    constructor(opts) {
13        var _a;
14        this.width = opts.width;
15        /* c8 ignore start */
16        this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true;
17        /* c8 ignore stop */
18        this.rows = [];
19    }
20    span(...args) {
21        const cols = this.div(...args);
22        cols.span = true;
23    }
24    resetOutput() {
25        this.rows = [];
26    }
27    div(...args) {
28        if (args.length === 0) {
29            this.div('');
30        }
31        if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') {
32            return this.applyLayoutDSL(args[0]);
33        }
34        const cols = args.map(arg => {
35            if (typeof arg === 'string') {
36                return this.colFromString(arg);
37            }
38            return arg;
39        });
40        this.rows.push(cols);
41        return cols;
42    }
43    shouldApplyLayoutDSL(...args) {
44        return args.length === 1 && typeof args[0] === 'string' &&
45            /[\t\n]/.test(args[0]);
46    }
47    applyLayoutDSL(str) {
48        const rows = str.split('\n').map(row => row.split('\t'));
49        let leftColumnWidth = 0;
50        // simple heuristic for layout, make sure the
51        // second column lines up along the left-hand.
52        // don't allow the first column to take up more
53        // than 50% of the screen.
54        rows.forEach(columns => {
55            if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) {
56                leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0]));
57            }
58        });
59        // generate a table:
60        //  replacing ' ' with padding calculations.
61        //  using the algorithmically generated width.
62        rows.forEach(columns => {
63            this.div(...columns.map((r, i) => {
64                return {
65                    text: r.trim(),
66                    padding: this.measurePadding(r),
67                    width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
68                };
69            }));
70        });
71        return this.rows[this.rows.length - 1];
72    }
73    colFromString(text) {
74        return {
75            text,
76            padding: this.measurePadding(text)
77        };
78    }
79    measurePadding(str) {
80        // measure padding without ansi escape codes
81        const noAnsi = mixin.stripAnsi(str);
82        return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
83    }
84    toString() {
85        const lines = [];
86        this.rows.forEach(row => {
87            this.rowToString(row, lines);
88        });
89        // don't display any lines with the
90        // hidden flag set.
91        return lines
92            .filter(line => !line.hidden)
93            .map(line => line.text)
94            .join('\n');
95    }
96    rowToString(row, lines) {
97        this.rasterize(row).forEach((rrow, r) => {
98            let str = '';
99            rrow.forEach((col, c) => {
100                const { width } = row[c]; // the width with padding.
101                const wrapWidth = this.negatePadding(row[c]); // the width without padding.
102                let ts = col; // temporary string used during alignment/padding.
103                if (wrapWidth > mixin.stringWidth(col)) {
104                    ts += ' '.repeat(wrapWidth - mixin.stringWidth(col));
105                }
106                // align the string within its column.
107                if (row[c].align && row[c].align !== 'left' && this.wrap) {
108                    const fn = align[row[c].align];
109                    ts = fn(ts, wrapWidth);
110                    if (mixin.stringWidth(ts) < wrapWidth) {
111                        /* c8 ignore start */
112                        const w = width || 0;
113                        /* c8 ignore stop */
114                        ts += ' '.repeat(w - mixin.stringWidth(ts) - 1);
115                    }
116                }
117                // apply border and padding to string.
118                const padding = row[c].padding || [0, 0, 0, 0];
119                if (padding[left]) {
120                    str += ' '.repeat(padding[left]);
121                }
122                str += addBorder(row[c], ts, '| ');
123                str += ts;
124                str += addBorder(row[c], ts, ' |');
125                if (padding[right]) {
126                    str += ' '.repeat(padding[right]);
127                }
128                // if prior row is span, try to render the
129                // current row on the prior line.
130                if (r === 0 && lines.length > 0) {
131                    str = this.renderInline(str, lines[lines.length - 1]);
132                }
133            });
134            // remove trailing whitespace.
135            lines.push({
136                text: str.replace(/ +$/, ''),
137                span: row.span
138            });
139        });
140        return lines;
141    }
142    // if the full 'source' can render in
143    // the target line, do so.
144    renderInline(source, previousLine) {
145        const match = source.match(/^ */);
146        /* c8 ignore start */
147        const leadingWhitespace = match ? match[0].length : 0;
148        /* c8 ignore stop */
149        const target = previousLine.text;
150        const targetTextWidth = mixin.stringWidth(target.trimEnd());
151        if (!previousLine.span) {
152            return source;
153        }
154        // if we're not applying wrapping logic,
155        // just always append to the span.
156        if (!this.wrap) {
157            previousLine.hidden = true;
158            return target + source;
159        }
160        if (leadingWhitespace < targetTextWidth) {
161            return source;
162        }
163        previousLine.hidden = true;
164        return target.trimEnd() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimStart();
165    }
166    rasterize(row) {
167        const rrows = [];
168        const widths = this.columnWidths(row);
169        let wrapped;
170        // word wrap all columns, and create
171        // a data-structure that is easy to rasterize.
172        row.forEach((col, c) => {
173            // leave room for left and right padding.
174            col.width = widths[c];
175            if (this.wrap) {
176                wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n');
177            }
178            else {
179                wrapped = col.text.split('\n');
180            }
181            if (col.border) {
182                wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
183                wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
184            }
185            // add top and bottom padding.
186            if (col.padding) {
187                wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
188                wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
189            }
190            wrapped.forEach((str, r) => {
191                if (!rrows[r]) {
192                    rrows.push([]);
193                }
194                const rrow = rrows[r];
195                for (let i = 0; i < c; i++) {
196                    if (rrow[i] === undefined) {
197                        rrow.push('');
198                    }
199                }
200                rrow.push(str);
201            });
202        });
203        return rrows;
204    }
205    negatePadding(col) {
206        /* c8 ignore start */
207        let wrapWidth = col.width || 0;
208        /* c8 ignore stop */
209        if (col.padding) {
210            wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
211        }
212        if (col.border) {
213            wrapWidth -= 4;
214        }
215        return wrapWidth;
216    }
217    columnWidths(row) {
218        if (!this.wrap) {
219            return row.map(col => {
220                return col.width || mixin.stringWidth(col.text);
221            });
222        }
223        let unset = row.length;
224        let remainingWidth = this.width;
225        // column widths can be set in config.
226        const widths = row.map(col => {
227            if (col.width) {
228                unset--;
229                remainingWidth -= col.width;
230                return col.width;
231            }
232            return undefined;
233        });
234        // any unset widths should be calculated.
235        /* c8 ignore start */
236        const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
237        /* c8 ignore stop */
238        return widths.map((w, i) => {
239            if (w === undefined) {
240                return Math.max(unsetWidth, _minWidth(row[i]));
241            }
242            return w;
243        });
244    }
245}
246function addBorder(col, ts, style) {
247    if (col.border) {
248        if (/[.']-+[.']/.test(ts)) {
249            return '';
250        }
251        if (ts.trim().length !== 0) {
252            return style;
253        }
254        return '  ';
255    }
256    return '';
257}
258// calculates the minimum width of
259// a column, based on padding preferences.
260function _minWidth(col) {
261    const padding = col.padding || [];
262    const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
263    if (col.border) {
264        return minWidth + 4;
265    }
266    return minWidth;
267}
268function getWindowWidth() {
269    /* c8 ignore start */
270    if (typeof process === 'object' && process.stdout && process.stdout.columns) {
271        return process.stdout.columns;
272    }
273    return 80;
274}
275/* c8 ignore stop */
276function alignRight(str, width) {
277    str = str.trim();
278    const strWidth = mixin.stringWidth(str);
279    if (strWidth < width) {
280        return ' '.repeat(width - strWidth) + str;
281    }
282    return str;
283}
284function alignCenter(str, width) {
285    str = str.trim();
286    const strWidth = mixin.stringWidth(str);
287    /* c8 ignore start */
288    if (strWidth >= width) {
289        return str;
290    }
291    /* c8 ignore stop */
292    return ' '.repeat((width - strWidth) >> 1) + str;
293}
294let mixin;
295function cliui(opts, _mixin) {
296    mixin = _mixin;
297    return new UI({
298        /* c8 ignore start */
299        width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(),
300        wrap: opts === null || opts === void 0 ? void 0 : opts.wrap
301        /* c8 ignore stop */
302    });
303}
304
305// Bootstrap cliui with CommonJS dependencies:
306const stringWidth = require('string-width-cjs');
307const stripAnsi = require('strip-ansi-cjs');
308const wrap = require('wrap-ansi-cjs');
309function ui(opts) {
310    return cliui(opts, {
311        stringWidth,
312        stripAnsi,
313        wrap
314    });
315}
316
317module.exports = ui;
318