309 lines
9.9 KiB
JavaScript
309 lines
9.9 KiB
JavaScript
|
"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;
|
||
|
}
|
||
|
|