3 var wcwidth = require('./width');
5 var _require = require('./utils');
7 var padRight = _require.padRight;
8 var padCenter = _require.padCenter;
9 var padLeft = _require.padLeft;
10 var splitIntoLines = _require.splitIntoLines;
11 var splitLongWords = _require.splitLongWords;
12 var truncateString = _require.truncateString;
14 var DEFAULT_HEADING_TRANSFORM = function DEFAULT_HEADING_TRANSFORM(key) {
15 return key.toUpperCase();
18 var DEFAULT_DATA_TRANSFORM = function DEFAULT_DATA_TRANSFORM(cell, column, index) {
22 var DEFAULTS = Object.freeze({
28 preserveNewLines: false,
31 headingTransform: DEFAULT_HEADING_TRANSFORM,
32 dataTransform: DEFAULT_DATA_TRANSFORM
35 module.exports = function (items) {
36 var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
38 var columnConfigs = options.config || {};
39 delete options.config; // remove config so doesn't appear on every column.
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
45 // Option defaults inheritance:
46 // options.config[columnName] => options => DEFAULTS
47 options = mixin({}, DEFAULTS, options);
49 options.config = options.config || Object.create(null);
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
57 items = toArray(items, columnNames);
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);
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]);
73 }, Object.create(null));
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';
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, ' ');
97 // merge all whitespace chars
98 result[columnName] = result[columnName].replace(/\s/gmi, ' ');
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);
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) {
119 changedKeys.forEach(function (key) {
120 return column[key] = col[key];
128 if (options.showHeaders) {
129 columnNames.forEach(function (columnName) {
130 var column = columns[columnName];
132 if (!column.showHeaders) {
133 headers[columnName] = '';
137 headers[columnName] = column.headingTransform(column.name);
139 items.unshift(headers);
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))));
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);
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);
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);
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))));
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)));
195 var rows = createRows(items, columns, columnNames, options.paddingChr); // merge lines into rows
197 return rows.reduce(function (output, row) {
198 return output.concat(row.reduce(function (rowOut, line) {
199 return rowOut.concat(line.join(options.columnSplitter));
201 }, []).map(function (line) {
202 return truncateString(line, maxLineWidth);
203 }).join(options.spacing);
207 * Convert wrapped lines into rows with padded values.
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
215 function createRows(items, columns, columnNames, paddingChr) {
216 return items.map(function (item) {
219 columnNames.forEach(function (columnName) {
220 numLines = Math.max(numLines, item[columnName].length);
222 // combine matching lines of each rows
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));
233 for (var i = 0; i < numLines; i++) {
243 * @return Object Object with properties mixed in.
249 if (Object.assign) return (_Object = Object).assign.apply(_Object, arguments);
250 return ObjectAssign.apply(undefined, arguments);
253 function ObjectAssign(target, firstSource) {
256 if (target === undefined || target === null) throw new TypeError("Cannot convert first argument to object");
258 var to = Object(target);
260 var hasPendingException = false;
261 var pendingException;
263 for (var i = 1; i < arguments.length; i++) {
264 var nextSource = arguments[i];
265 if (nextSource === undefined || nextSource === null) continue;
267 var keysArray = Object.keys(Object(nextSource));
268 for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
269 var nextKey = keysArray[nextIndex];
271 var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
272 if (desc !== undefined && desc.enumerable) to[nextKey] = nextSource[nextKey];
274 if (!hasPendingException) {
275 hasPendingException = true;
276 pendingException = e;
281 if (hasPendingException) throw pendingException;
287 * Adapted from String.prototype.endsWith polyfill.
290 function 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;
297 function toArray(items, columnNames) {
298 if (Array.isArray(items)) return items;
300 for (var key in items) {
302 item[columnNames[0] || 'key'] = key;
303 item[columnNames[1] || 'value'] = items[key];