nice-table
Version:
A non-overflowing `console.table` alternative with customization options.
196 lines • 8.88 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTable = void 0;
const wrap_ansi_1 = __importDefault(require("wrap-ansi"));
const string_width_1 = __importDefault(require("string-width"));
function createTable(items, keys, { horizontalAlignment = 'middle', verticalAlignment = 'middle', fullWidth = false, columnSizing = 'stretch', maxWidth = 80, indexColumn = false, throwIfTooSmall = true, stringify = String, } = {}) {
let columnCount = keys.length;
const columnNames = keys.map((key) => stringify(key));
const tableContent = items.map((item) => keys.map((key) => stringify(item[key])));
if (indexColumn) {
columnNames.unshift('(index)');
tableContent.forEach((row, i) => row.unshift(i.toString()));
columnCount++;
}
const minimumWidth = 4 * columnCount + 1;
if (maxWidth < minimumWidth) {
const message = `The table does not fit. The width should be set to at least ${4 * columnCount + 1} (4 * ColumnCount + 1) for this table to fit (received width ${maxWidth}).`;
if (throwIfTooSmall) {
throw new Error(message);
}
else {
return message;
}
}
const availableColumnWidth = maxWidth - columnCount - 1; // each key/column has a │ on the left, and there is one │ at the end of each row
const averageColumnWidth = Math.floor(availableColumnWidth / columnCount);
const columnWidths = columnNames.map((columnName, i) => Math.max((0, string_width_1.default)(columnName), ...tableContent.map((row) => (0, string_width_1.default)(row[i]))) + 2);
let overflow = columnWidths.reduce((a, b) => a + b, 0) - availableColumnWidth;
switch (columnSizing) {
case 'stretch': {
if (overflow > 0) {
shrinkColumns(columnWidths, averageColumnWidth, overflow);
overflow = 0;
}
break;
}
case 'even': {
const maxColumnWidth = Math.max(...columnWidths);
for (let i = 0; i < columnWidths.length; i++) {
columnWidths[i] = maxColumnWidth;
}
overflow = maxColumnWidth * columnCount - availableColumnWidth;
if (overflow > 0) {
shrinkColumns(columnWidths, averageColumnWidth, overflow);
overflow = 0;
}
break;
}
default:
throw new Error(`Unknown column sizing '${columnSizing}'`);
}
if (fullWidth && overflow < 0) {
growColumns(columnWidths, averageColumnWidth, -overflow);
overflow = 0;
}
for (const columnWidth of columnWidths) {
if (columnWidth < 3) {
throw new Error('Table does not fit. Please file a bug report as this error should not happen: https://github.com/timvandam/nice-table/issues/new');
}
}
return [
createTableTop(columnWidths),
createTableColumnNames(columnNames, columnWidths, horizontalAlignment, verticalAlignment),
createTableRows(tableContent, columnWidths, horizontalAlignment, verticalAlignment),
createTableBottom(columnWidths),
]
.filter((part) => part.trim().length > 0)
.join('\n');
}
exports.createTable = createTable;
const TOP_LEFT = '┌';
const TOP_RIGHT = '┐';
const TOP_MIDDLE = '┬';
const BOTTOM_LEFT = '└';
const BOTTOM_RIGHT = '┘';
const BOTTOM_MIDDLE = '┴';
const LEFT_MIDDLE = '├';
const RIGHT_MIDDLE = '┤';
const MIDDLE = '┼';
const HORIZONTAL = '─';
const VERTICAL = '│';
function createTableTop(columnWidths) {
return (TOP_LEFT + columnWidths.map((size) => HORIZONTAL.repeat(size)).join(TOP_MIDDLE) + TOP_RIGHT);
}
function createTableMiddleLine(columnWidths) {
return (LEFT_MIDDLE + columnWidths.map((size) => HORIZONTAL.repeat(size)).join(MIDDLE) + RIGHT_MIDDLE);
}
function createTableBottom(columnWidths) {
return (BOTTOM_LEFT +
columnWidths.map((size) => HORIZONTAL.repeat(size)).join(BOTTOM_MIDDLE) +
BOTTOM_RIGHT);
}
function centerText(str, width) {
const strLength = (0, string_width_1.default)(str);
const leftPadding = Math.floor((width - strLength) / 2);
const rightPadding = width - strLength - leftPadding;
return ' '.repeat(leftPadding) + str + ' '.repeat(rightPadding);
}
function createRow(cells, columnWidths, horizontalAlignment) {
return (VERTICAL +
columnWidths
.map((columnWidth, i) => {
if (horizontalAlignment === 'left') {
return (' ' + cells[i]).padEnd(columnWidth, ' ');
}
else if (horizontalAlignment === 'right') {
return (cells[i] + ' ').padStart(columnWidth, ' ');
}
else {
return centerText(cells[i], columnWidth);
}
})
.join(VERTICAL) +
VERTICAL);
}
function createMultiLineRows(row, columnWidths, horizontalAlignment, verticalAlignment) {
var _a;
const cells = [];
let rowHeight = 0;
// Split single cells into multiple cells if they are too long
for (let i = 0; i < columnWidths.length; i++) {
const columnWidth = columnWidths[i];
const cellLines = (0, wrap_ansi_1.default)(row[i], columnWidth - 2, {
hard: true,
trim: true,
wordWrap: true,
}).split('\n');
cells.push(cellLines);
rowHeight = Math.max(rowHeight, cellLines.length);
}
// Align cells vertically
if (verticalAlignment !== 'top') {
for (let i = 0; i < cells.length; i++) {
const padding = (_a = {
top: 0,
middle: Math.floor((rowHeight - cells[i].length) / 2),
bottom: rowHeight - cells[i].length,
}[verticalAlignment]) !== null && _a !== void 0 ? _a : 0;
cells[i].unshift(...Array(padding).fill(''));
}
}
const cellRows = [];
for (let i = 0; i < rowHeight; i++) {
cellRows.push(cells.map((cell) => { var _a; return (_a = cell[i]) !== null && _a !== void 0 ? _a : ''; }));
}
return cellRows.map((row) => createRow(row, columnWidths, horizontalAlignment)).join('\n');
}
function createTableRows(tableContent, columnWidths, horizontalAlignment, verticalAlignment) {
return tableContent
.map((row) => createMultiLineRows(row, columnWidths, horizontalAlignment, verticalAlignment))
.join('\n');
}
function createTableColumnNames(columnNames, columnWidths, horizontalAlignment, verticalAlignment) {
return (createMultiLineRows(columnNames, columnWidths, horizontalAlignment, verticalAlignment) +
'\n' +
createTableMiddleLine(columnWidths));
}
/**
* Shrink columns to fit the table width.
* Can lead to negative widths columns if the minimum width is too low (taking $AvailableWidth / ColumnCount$ always works).
*/
function shrinkColumns(columnWidths, minimumWidth, overflow) {
const bigColumnIndices = Array.from({ length: columnWidths.length }, (_, i) => i).filter((i) => columnWidths[i] > minimumWidth);
let bigColumnsWidth = bigColumnIndices.reduce((totalWidth, columnIndex) => totalWidth + columnWidths[columnIndex], 0);
const shrinkFactor = 1 - overflow / (bigColumnsWidth - bigColumnIndices.length * minimumWidth);
for (const columnIndex of bigColumnIndices.slice(0, -1)) {
const currentColumnWidth = columnWidths[columnIndex];
const fixedWidth = Math.max(minimumWidth, Math.floor(shrinkFactor * currentColumnWidth));
const widthReduction = currentColumnWidth - fixedWidth;
columnWidths[columnIndex] = fixedWidth;
overflow -= widthReduction;
bigColumnsWidth -= widthReduction;
}
columnWidths[bigColumnIndices[bigColumnIndices.length - 1]] -= overflow;
}
/**
* Grow small columns by a certain amount.
*/
function growColumns(columnWidths, minimumWidth, growth) {
const smallColumnIndices = Array.from({ length: columnWidths.length }, (_, i) => i).filter((i) => columnWidths[i] < minimumWidth);
let smallColumnsWidth = smallColumnIndices.reduce((totalWidth, columnIndex) => totalWidth + columnWidths[columnIndex], 0);
const growFactor = 1 + growth / smallColumnsWidth;
for (const columnIndex of smallColumnIndices.slice(0, -1)) {
const currentColumnWidth = columnWidths[columnIndex];
const fixedWidth = Math.floor(growFactor * currentColumnWidth);
const widthGrowth = fixedWidth - currentColumnWidth;
columnWidths[columnIndex] = fixedWidth;
growth -= widthGrowth;
smallColumnsWidth += widthGrowth;
}
columnWidths[smallColumnIndices[smallColumnIndices.length - 1]] += growth;
}
//# sourceMappingURL=index.js.map
;