• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"use strict";
2
3var wcwidth = require('./width');
4
5var _require = require('./utils');
6
7var padRight = _require.padRight;
8var padCenter = _require.padCenter;
9var padLeft = _require.padLeft;
10var splitIntoLines = _require.splitIntoLines;
11var splitLongWords = _require.splitLongWords;
12var truncateString = _require.truncateString;
13
14var DEFAULT_HEADING_TRANSFORM = function DEFAULT_HEADING_TRANSFORM(key) {
15  return key.toUpperCase();
16};
17
18var DEFAULT_DATA_TRANSFORM = function DEFAULT_DATA_TRANSFORM(cell, column, index) {
19  return cell;
20};
21
22var DEFAULTS = Object.freeze({
23  maxWidth: Infinity,
24  minWidth: 0,
25  columnSplitter: ' ',
26  truncate: false,
27  truncateMarker: '…',
28  preserveNewLines: false,
29  paddingChr: ' ',
30  showHeaders: true,
31  headingTransform: DEFAULT_HEADING_TRANSFORM,
32  dataTransform: DEFAULT_DATA_TRANSFORM
33});
34
35module.exports = function (items) {
36  var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
37
38  var columnConfigs = options.config || {};
39  delete options.config; // remove config so doesn't appear on every column.
40
41  var maxLineWidth = options.maxLineWidth || Infinity;
42  if (maxLineWidth === 'auto') maxLineWidth = process.stdout.columns || Infinity;
43  delete options.maxLineWidth; // this is a line control option, don't pass it to column
44
45  // Option defaults inheritance:
46  // options.config[columnName] => options => DEFAULTS
47  options = mixin({}, DEFAULTS, options);
48
49  options.config = options.config || Object.create(null);
50
51  options.spacing = options.spacing || '\n'; // probably useless
52  options.preserveNewLines = !!options.preserveNewLines;
53  options.showHeaders = !!options.showHeaders;
54  options.columns = options.columns || options.include; // alias include/columns, prefer columns if supplied
55  var columnNames = options.columns || []; // optional user-supplied columns to include
56
57  items = toArray(items, columnNames);
58
59  // if not suppled column names, automatically determine columns from data keys
60  if (!columnNames.length) {
61    items.forEach(function (item) {
62      for (var columnName in item) {
63        if (columnNames.indexOf(columnName) === -1) columnNames.push(columnName);
64      }
65    });
66  }
67
68  // initialize column defaults (each column inherits from options.config)
69  var columns = columnNames.reduce(function (columns, columnName) {
70    var column = Object.create(options);
71    columns[columnName] = mixin(column, columnConfigs[columnName]);
72    return columns;
73  }, Object.create(null));
74
75  // sanitize column settings
76  columnNames.forEach(function (columnName) {
77    var column = columns[columnName];
78    column.name = columnName;
79    column.maxWidth = Math.ceil(column.maxWidth);
80    column.minWidth = Math.ceil(column.minWidth);
81    column.truncate = !!column.truncate;
82    column.align = column.align || 'left';
83  });
84
85  // sanitize data
86  items = items.map(function (item) {
87    var result = Object.create(null);
88    columnNames.forEach(function (columnName) {
89      // null/undefined -> ''
90      result[columnName] = item[columnName] != null ? item[columnName] : '';
91      // toString everything
92      result[columnName] = '' + result[columnName];
93      if (columns[columnName].preserveNewLines) {
94        // merge non-newline whitespace chars
95        result[columnName] = result[columnName].replace(/[^\S\n]/gmi, ' ');
96      } else {
97        // merge all whitespace chars
98        result[columnName] = result[columnName].replace(/\s/gmi, ' ');
99      }
100    });
101    return result;
102  });
103
104  // transform data cells
105  columnNames.forEach(function (columnName) {
106    var column = columns[columnName];
107    items = items.map(function (item, index) {
108      var col = Object.create(column);
109      item[columnName] = column.dataTransform(item[columnName], col, index);
110
111      var changedKeys = Object.keys(col);
112      // disable default heading transform if we wrote to column.name
113      if (changedKeys.indexOf('name') !== -1) {
114        if (column.headingTransform !== DEFAULT_HEADING_TRANSFORM) return;
115        column.headingTransform = function (heading) {
116          return heading;
117        };
118      }
119      changedKeys.forEach(function (key) {
120        return column[key] = col[key];
121      });
122      return item;
123    });
124  });
125
126  // add headers
127  var headers = {};
128  if (options.showHeaders) {
129    columnNames.forEach(function (columnName) {
130      var column = columns[columnName];
131
132      if (!column.showHeaders) {
133        headers[columnName] = '';
134        return;
135      }
136
137      headers[columnName] = column.headingTransform(column.name);
138    });
139    items.unshift(headers);
140  }
141  // get actual max-width between min & max
142  // based on length of data in columns
143  columnNames.forEach(function (columnName) {
144    var column = columns[columnName];
145    column.width = items.map(function (item) {
146      return item[columnName];
147    }).reduce(function (min, cur) {
148      // if already at maxWidth don't bother testing
149      if (min >= column.maxWidth) return min;
150      return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))));
151    }, 0);
152  });
153
154  // split long words so they can break onto multiple lines
155  columnNames.forEach(function (columnName) {
156    var column = columns[columnName];
157    items = items.map(function (item) {
158      item[columnName] = splitLongWords(item[columnName], column.width, column.truncateMarker);
159      return item;
160    });
161  });
162
163  // wrap long lines. each item is now an array of lines.
164  columnNames.forEach(function (columnName) {
165    var column = columns[columnName];
166    items = items.map(function (item, index) {
167      var cell = item[columnName];
168      item[columnName] = splitIntoLines(cell, column.width);
169
170      // if truncating required, only include first line + add truncation char
171      if (column.truncate && item[columnName].length > 1) {
172        item[columnName] = splitIntoLines(cell, column.width - wcwidth(column.truncateMarker));
173        var firstLine = item[columnName][0];
174        if (!endsWith(firstLine, column.truncateMarker)) item[columnName][0] += column.truncateMarker;
175        item[columnName] = item[columnName].slice(0, 1);
176      }
177      return item;
178    });
179  });
180
181  // recalculate column widths from truncated output/lines
182  columnNames.forEach(function (columnName) {
183    var column = columns[columnName];
184    column.width = items.map(function (item) {
185      return item[columnName].reduce(function (min, cur) {
186        if (min >= column.maxWidth) return min;
187        return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))));
188      }, 0);
189    }).reduce(function (min, cur) {
190      if (min >= column.maxWidth) return min;
191      return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur)));
192    }, 0);
193  });
194
195  var rows = createRows(items, columns, columnNames, options.paddingChr); // merge lines into rows
196  // conceive output
197  return rows.reduce(function (output, row) {
198    return output.concat(row.reduce(function (rowOut, line) {
199      return rowOut.concat(line.join(options.columnSplitter));
200    }, []));
201  }, []).map(function (line) {
202    return truncateString(line, maxLineWidth);
203  }).join(options.spacing);
204};
205
206/**
207 * Convert wrapped lines into rows with padded values.
208 *
209 * @param Array items data to process
210 * @param Array columns column width settings for wrapping
211 * @param Array columnNames column ordering
212 * @return Array items wrapped in arrays, corresponding to lines
213 */
214
215function createRows(items, columns, columnNames, paddingChr) {
216  return items.map(function (item) {
217    var row = [];
218    var numLines = 0;
219    columnNames.forEach(function (columnName) {
220      numLines = Math.max(numLines, item[columnName].length);
221    });
222    // combine matching lines of each rows
223
224    var _loop = function _loop(i) {
225      row[i] = row[i] || [];
226      columnNames.forEach(function (columnName) {
227        var column = columns[columnName];
228        var val = item[columnName][i] || ''; // || '' ensures empty columns get padded
229        if (column.align === 'right') row[i].push(padLeft(val, column.width, paddingChr));else if (column.align === 'center' || column.align === 'centre') row[i].push(padCenter(val, column.width, paddingChr));else row[i].push(padRight(val, column.width, paddingChr));
230      });
231    };
232
233    for (var i = 0; i < numLines; i++) {
234      _loop(i);
235    }
236    return row;
237  });
238}
239
240/**
241 * Object.assign
242 *
243 * @return Object Object with properties mixed in.
244 */
245
246function mixin() {
247  var _Object;
248
249  if (Object.assign) return (_Object = Object).assign.apply(_Object, arguments);
250  return ObjectAssign.apply(undefined, arguments);
251}
252
253function ObjectAssign(target, firstSource) {
254  "use strict";
255
256  if (target === undefined || target === null) throw new TypeError("Cannot convert first argument to object");
257
258  var to = Object(target);
259
260  var hasPendingException = false;
261  var pendingException;
262
263  for (var i = 1; i < arguments.length; i++) {
264    var nextSource = arguments[i];
265    if (nextSource === undefined || nextSource === null) continue;
266
267    var keysArray = Object.keys(Object(nextSource));
268    for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
269      var nextKey = keysArray[nextIndex];
270      try {
271        var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
272        if (desc !== undefined && desc.enumerable) to[nextKey] = nextSource[nextKey];
273      } catch (e) {
274        if (!hasPendingException) {
275          hasPendingException = true;
276          pendingException = e;
277        }
278      }
279    }
280
281    if (hasPendingException) throw pendingException;
282  }
283  return to;
284}
285
286/**
287 * Adapted from String.prototype.endsWith polyfill.
288 */
289
290function endsWith(target, searchString, position) {
291  position = position || target.length;
292  position = position - searchString.length;
293  var lastIndex = target.lastIndexOf(searchString);
294  return lastIndex !== -1 && lastIndex === position;
295}
296
297function toArray(items, columnNames) {
298  if (Array.isArray(items)) return items;
299  var rows = [];
300  for (var key in items) {
301    var item = {};
302    item[columnNames[0] || 'key'] = key;
303    item[columnNames[1] || 'value'] = items[key];
304    rows.push(item);
305  }
306  return rows;
307}
308
309