| "use strict"; |
| |
| var wcwidth = require('./width'); |
| |
| var _require = require('./utils'); |
| |
| var padRight = _require.padRight; |
| var padCenter = _require.padCenter; |
| var padLeft = _require.padLeft; |
| var splitIntoLines = _require.splitIntoLines; |
| var splitLongWords = _require.splitLongWords; |
| var truncateString = _require.truncateString; |
| |
| var DEFAULT_HEADING_TRANSFORM = function DEFAULT_HEADING_TRANSFORM(key) { |
| return key.toUpperCase(); |
| }; |
| |
| var DEFAULT_DATA_TRANSFORM = function DEFAULT_DATA_TRANSFORM(cell, column, index) { |
| return cell; |
| }; |
| |
| var DEFAULTS = Object.freeze({ |
| maxWidth: Infinity, |
| minWidth: 0, |
| columnSplitter: ' ', |
| truncate: false, |
| truncateMarker: '…', |
| preserveNewLines: false, |
| paddingChr: ' ', |
| showHeaders: true, |
| headingTransform: DEFAULT_HEADING_TRANSFORM, |
| dataTransform: DEFAULT_DATA_TRANSFORM |
| }); |
| |
| module.exports = function (items) { |
| var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; |
| |
| var columnConfigs = options.config || {}; |
| delete options.config; // remove config so doesn't appear on every column. |
| |
| var maxLineWidth = options.maxLineWidth || Infinity; |
| if (maxLineWidth === 'auto') maxLineWidth = process.stdout.columns || Infinity; |
| delete options.maxLineWidth; // this is a line control option, don't pass it to column |
| |
| // Option defaults inheritance: |
| // options.config[columnName] => options => DEFAULTS |
| options = mixin({}, DEFAULTS, options); |
| |
| options.config = options.config || Object.create(null); |
| |
| options.spacing = options.spacing || '\n'; // probably useless |
| options.preserveNewLines = !!options.preserveNewLines; |
| options.showHeaders = !!options.showHeaders; |
| options.columns = options.columns || options.include; // alias include/columns, prefer columns if supplied |
| var columnNames = options.columns || []; // optional user-supplied columns to include |
| |
| items = toArray(items, columnNames); |
| |
| // if not suppled column names, automatically determine columns from data keys |
| if (!columnNames.length) { |
| items.forEach(function (item) { |
| for (var columnName in item) { |
| if (columnNames.indexOf(columnName) === -1) columnNames.push(columnName); |
| } |
| }); |
| } |
| |
| // initialize column defaults (each column inherits from options.config) |
| var columns = columnNames.reduce(function (columns, columnName) { |
| var column = Object.create(options); |
| columns[columnName] = mixin(column, columnConfigs[columnName]); |
| return columns; |
| }, Object.create(null)); |
| |
| // sanitize column settings |
| columnNames.forEach(function (columnName) { |
| var column = columns[columnName]; |
| column.name = columnName; |
| column.maxWidth = Math.ceil(column.maxWidth); |
| column.minWidth = Math.ceil(column.minWidth); |
| column.truncate = !!column.truncate; |
| column.align = column.align || 'left'; |
| }); |
| |
| // sanitize data |
| items = items.map(function (item) { |
| var result = Object.create(null); |
| columnNames.forEach(function (columnName) { |
| // null/undefined -> '' |
| result[columnName] = item[columnName] != null ? item[columnName] : ''; |
| // toString everything |
| result[columnName] = '' + result[columnName]; |
| if (columns[columnName].preserveNewLines) { |
| // merge non-newline whitespace chars |
| result[columnName] = result[columnName].replace(/[^\S\n]/gmi, ' '); |
| } else { |
| // merge all whitespace chars |
| result[columnName] = result[columnName].replace(/\s/gmi, ' '); |
| } |
| }); |
| return result; |
| }); |
| |
| // transform data cells |
| columnNames.forEach(function (columnName) { |
| var column = columns[columnName]; |
| items = items.map(function (item, index) { |
| var col = Object.create(column); |
| item[columnName] = column.dataTransform(item[columnName], col, index); |
| |
| var changedKeys = Object.keys(col); |
| // disable default heading transform if we wrote to column.name |
| if (changedKeys.indexOf('name') !== -1) { |
| if (column.headingTransform !== DEFAULT_HEADING_TRANSFORM) return; |
| column.headingTransform = function (heading) { |
| return heading; |
| }; |
| } |
| changedKeys.forEach(function (key) { |
| return column[key] = col[key]; |
| }); |
| return item; |
| }); |
| }); |
| |
| // add headers |
| var headers = {}; |
| if (options.showHeaders) { |
| columnNames.forEach(function (columnName) { |
| var column = columns[columnName]; |
| |
| if (!column.showHeaders) { |
| headers[columnName] = ''; |
| return; |
| } |
| |
| headers[columnName] = column.headingTransform(column.name); |
| }); |
| items.unshift(headers); |
| } |
| // get actual max-width between min & max |
| // based on length of data in columns |
| columnNames.forEach(function (columnName) { |
| var column = columns[columnName]; |
| column.width = items.map(function (item) { |
| return item[columnName]; |
| }).reduce(function (min, cur) { |
| // if already at maxWidth don't bother testing |
| if (min >= column.maxWidth) return min; |
| return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur)))); |
| }, 0); |
| }); |
| |
| // split long words so they can break onto multiple lines |
| columnNames.forEach(function (columnName) { |
| var column = columns[columnName]; |
| items = items.map(function (item) { |
| item[columnName] = splitLongWords(item[columnName], column.width, column.truncateMarker); |
| return item; |
| }); |
| }); |
| |
| // wrap long lines. each item is now an array of lines. |
| columnNames.forEach(function (columnName) { |
| var column = columns[columnName]; |
| items = items.map(function (item, index) { |
| var cell = item[columnName]; |
| item[columnName] = splitIntoLines(cell, column.width); |
| |
| // if truncating required, only include first line + add truncation char |
| if (column.truncate && item[columnName].length > 1) { |
| item[columnName] = splitIntoLines(cell, column.width - wcwidth(column.truncateMarker)); |
| var firstLine = item[columnName][0]; |
| if (!endsWith(firstLine, column.truncateMarker)) item[columnName][0] += column.truncateMarker; |
| item[columnName] = item[columnName].slice(0, 1); |
| } |
| return item; |
| }); |
| }); |
| |
| // recalculate column widths from truncated output/lines |
| columnNames.forEach(function (columnName) { |
| var column = columns[columnName]; |
| column.width = items.map(function (item) { |
| return item[columnName].reduce(function (min, cur) { |
| if (min >= column.maxWidth) return min; |
| return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur)))); |
| }, 0); |
| }).reduce(function (min, cur) { |
| if (min >= column.maxWidth) return min; |
| return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur))); |
| }, 0); |
| }); |
| |
| var rows = createRows(items, columns, columnNames, options.paddingChr); // merge lines into rows |
| // conceive output |
| return rows.reduce(function (output, row) { |
| return output.concat(row.reduce(function (rowOut, line) { |
| return rowOut.concat(line.join(options.columnSplitter)); |
| }, [])); |
| }, []).map(function (line) { |
| return truncateString(line, maxLineWidth); |
| }).join(options.spacing); |
| }; |
| |
| /** |
| * Convert wrapped lines into rows with padded values. |
| * |
| * @param Array items data to process |
| * @param Array columns column width settings for wrapping |
| * @param Array columnNames column ordering |
| * @return Array items wrapped in arrays, corresponding to lines |
| */ |
| |
| function createRows(items, columns, columnNames, paddingChr) { |
| return items.map(function (item) { |
| var row = []; |
| var numLines = 0; |
| columnNames.forEach(function (columnName) { |
| numLines = Math.max(numLines, item[columnName].length); |
| }); |
| // combine matching lines of each rows |
| |
| var _loop = function _loop(i) { |
| row[i] = row[i] || []; |
| columnNames.forEach(function (columnName) { |
| var column = columns[columnName]; |
| var val = item[columnName][i] || ''; // || '' ensures empty columns get padded |
| 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)); |
| }); |
| }; |
| |
| for (var i = 0; i < numLines; i++) { |
| _loop(i); |
| } |
| return row; |
| }); |
| } |
| |
| /** |
| * Object.assign |
| * |
| * @return Object Object with properties mixed in. |
| */ |
| |
| function mixin() { |
| var _Object; |
| |
| if (Object.assign) return (_Object = Object).assign.apply(_Object, arguments); |
| return ObjectAssign.apply(undefined, arguments); |
| } |
| |
| function ObjectAssign(target, firstSource) { |
| "use strict"; |
| |
| if (target === undefined || target === null) throw new TypeError("Cannot convert first argument to object"); |
| |
| var to = Object(target); |
| |
| var hasPendingException = false; |
| var pendingException; |
| |
| for (var i = 1; i < arguments.length; i++) { |
| var nextSource = arguments[i]; |
| if (nextSource === undefined || nextSource === null) continue; |
| |
| var keysArray = Object.keys(Object(nextSource)); |
| for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { |
| var nextKey = keysArray[nextIndex]; |
| try { |
| var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); |
| if (desc !== undefined && desc.enumerable) to[nextKey] = nextSource[nextKey]; |
| } catch (e) { |
| if (!hasPendingException) { |
| hasPendingException = true; |
| pendingException = e; |
| } |
| } |
| } |
| |
| if (hasPendingException) throw pendingException; |
| } |
| return to; |
| } |
| |
| /** |
| * Adapted from String.prototype.endsWith polyfill. |
| */ |
| |
| function endsWith(target, searchString, position) { |
| position = position || target.length; |
| position = position - searchString.length; |
| var lastIndex = target.lastIndexOf(searchString); |
| return lastIndex !== -1 && lastIndex === position; |
| } |
| |
| function toArray(items, columnNames) { |
| if (Array.isArray(items)) return items; |
| var rows = []; |
| for (var key in items) { |
| var item = {}; |
| item[columnNames[0] || 'key'] = key; |
| item[columnNames[1] || 'value'] = items[key]; |
| rows.push(item); |
| } |
| return rows; |
| } |
| |