--- /dev/null
+"use strict"
+
+const wcwidth = require('./width')
+const {
+ padRight,
+ padCenter,
+ padLeft,
+ splitIntoLines,
+ splitLongWords,
+ truncateString
+} = require('./utils')
+
+const DEFAULT_HEADING_TRANSFORM = key => key.toUpperCase()
+
+const DEFAULT_DATA_TRANSFORM = (cell, column, index) => cell
+
+const 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, options = {}) {
+
+ let columnConfigs = options.config || {}
+ delete options.config // remove config so doesn't appear on every column.
+
+ let 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
+ let 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 (let columnName in item) {
+ if (columnNames.indexOf(columnName) === -1) columnNames.push(columnName)
+ }
+ })
+ }
+
+ // initialize column defaults (each column inherits from options.config)
+ let columns = columnNames.reduce((columns, columnName) => {
+ let column = Object.create(options)
+ columns[columnName] = mixin(column, columnConfigs[columnName])
+ return columns
+ }, Object.create(null))
+
+ // sanitize column settings
+ columnNames.forEach(columnName => {
+ let 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(item => {
+ let result = Object.create(null)
+ columnNames.forEach(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(columnName => {
+ let column = columns[columnName]
+ items = items.map((item, index) => {
+ let col = Object.create(column)
+ item[columnName] = column.dataTransform(item[columnName], col, index)
+
+ let 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 = heading => heading
+ }
+ changedKeys.forEach(key => column[key] = col[key])
+ return item
+ })
+ })
+
+ // add headers
+ let headers = {}
+ if(options.showHeaders) {
+ columnNames.forEach(columnName => {
+ let 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(columnName => {
+ let column = columns[columnName]
+ column.width = items
+ .map(item => item[columnName])
+ .reduce((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(columnName => {
+ let column = columns[columnName]
+ items = items.map(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(columnName => {
+ let column = columns[columnName]
+ items = items.map((item, index) => {
+ let 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))
+ let 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(columnName => {
+ let column = columns[columnName]
+ column.width = items.map(item => {
+ return item[columnName].reduce((min, cur) => {
+ if (min >= column.maxWidth) return min
+ return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))))
+ }, 0)
+ }).reduce((min, cur) => {
+ if (min >= column.maxWidth) return min
+ return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur)))
+ }, 0)
+ })
+
+
+ let rows = createRows(items, columns, columnNames, options.paddingChr) // merge lines into rows
+ // conceive output
+ return rows.reduce((output, row) => {
+ return output.concat(row.reduce((rowOut, line) => {
+ return rowOut.concat(line.join(options.columnSplitter))
+ }, []))
+ }, [])
+ .map(line => 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(item => {
+ let row = []
+ let numLines = 0
+ columnNames.forEach(columnName => {
+ numLines = Math.max(numLines, item[columnName].length)
+ })
+ // combine matching lines of each rows
+ for (let i = 0; i < numLines; i++) {
+ row[i] = row[i] || []
+ columnNames.forEach(columnName => {
+ let column = columns[columnName]
+ let 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))
+ })
+ }
+ return row
+ })
+}
+
+/**
+ * Object.assign
+ *
+ * @return Object Object with properties mixed in.
+ */
+
+function mixin(...args) {
+ if (Object.assign) return Object.assign(...args)
+ return ObjectAssign(...args)
+}
+
+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;
+ let lastIndex = target.lastIndexOf(searchString);
+ return lastIndex !== -1 && lastIndex === position;
+}
+
+
+function toArray(items, columnNames) {
+ if (Array.isArray(items)) return items
+ let rows = []
+ for (let key in items) {
+ let item = {}
+ item[columnNames[0] || 'key'] = key
+ item[columnNames[1] || 'value'] = items[key]
+ rows.push(item)
+ }
+ return rows
+}