index.js
  1  'use strict';
  2  const align = {
  3      right: alignRight,
  4      center: alignCenter
  5  };
  6  const top = 0;
  7  const right = 1;
  8  const bottom = 2;
  9  const left = 3;
 10  export 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  }
234  function 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.
248  function _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  }
256  function 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  }
263  function 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  }
271  function 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  }
280  let mixin;
281  export 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  }