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