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