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