knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
100 lines (99 loc) • 4.51 kB
JavaScript
import { stripVTControlCharacters } from 'node:util';
import { pad, truncate, truncateStart } from './string.js';
const DEFAULT_MAX_WIDTH = process.stdout.columns || 120;
const MIN_TRUNCATED_WIDTH = 4;
const COLUMN_SEPARATOR = ' ';
export class Table {
columns = [];
rows = [];
header;
maxWidth;
truncateStart = [];
noTruncate = [];
constructor(options) {
this.header = options?.header ?? false;
this.maxWidth = options?.maxWidth || DEFAULT_MAX_WIDTH;
this.truncateStart = options?.truncateStart || [];
this.noTruncate = options?.noTruncate || [];
}
row() {
this.rows.push({});
return this;
}
cell(column, value, formatter) {
if (!this.columns.includes(column))
this.columns.push(column);
const row = this.rows[this.rows.length - 1];
const align = typeof value === 'number' ? 'right' : 'left';
const formatted = formatter ? formatter(value) : undefined;
row[column] = { value, formatted, align };
return this;
}
sort(column) {
this.rows.sort((a, b) => {
const [columnName, order] = column.split('|');
const vA = a[columnName].value;
const vB = b[columnName].value;
if (typeof vA === 'string' && typeof vB === 'string')
return (order === 'desc' ? -1 : 1) * vA.localeCompare(vB);
if (typeof vA === 'number' && typeof vB === 'number')
return order === 'desc' ? vB - vA : vA - vB;
return !vA ? 1 : !vB ? -1 : 0;
});
return this;
}
toCells() {
const columns = this.columns.filter(col => this.rows.some(row => typeof row[col].value === 'string' || typeof row[col].value === 'number'));
if (this.header) {
const headerRow = {};
const linesRow = {};
for (const col of columns) {
headerRow[col] = { value: col, align: this.rows[0][col].align === 'right' ? 'center' : 'left' };
linesRow[col] = { value: '', fill: '-' };
}
this.rows.unshift(linesRow);
this.rows.unshift(headerRow);
}
const columnWidths = columns.reduce((acc, col) => {
acc[col] = Math.max(...this.rows.map(row => row[col]?.formatted
? stripVTControlCharacters(row[col].formatted).length
: String(row[col]?.value || '').length));
return acc;
}, {});
const separatorWidth = (columns.length - 1) * COLUMN_SEPARATOR.length;
const totalWidth = Object.values(columnWidths).reduce((sum, width) => sum + width, 0) + separatorWidth;
if (totalWidth > this.maxWidth) {
const reservedWidth = columns
.filter(col => this.noTruncate.includes(col))
.reduce((sum, col) => sum + columnWidths[col], 0);
const truncatableColumns = columns.filter(col => !this.noTruncate.includes(col));
const minWidth = truncatableColumns.length * 4;
const availableWidth = this.maxWidth - separatorWidth - reservedWidth - minWidth;
const truncatableWidth = truncatableColumns.reduce((sum, col) => sum + columnWidths[col], 0) - minWidth;
const reduction = availableWidth / truncatableWidth;
let roundingDiff = availableWidth;
for (const col of truncatableColumns) {
const reducedWidth = MIN_TRUNCATED_WIDTH + Math.floor((columnWidths[col] - MIN_TRUNCATED_WIDTH) * reduction);
columnWidths[col] = reducedWidth;
roundingDiff -= reducedWidth - MIN_TRUNCATED_WIDTH;
}
if (roundingDiff > 0) {
columnWidths[truncatableColumns.length > 0 ? truncatableColumns[0] : columns[0]] += roundingDiff;
}
}
return this.rows.map(row => columns.map((col, index) => {
const cell = row[col];
const width = columnWidths[col];
const fill = cell.fill || ' ';
const padded = pad(String(cell.formatted || cell.value || ''), width, fill, cell.align);
const truncated = this.truncateStart.includes(col) ? truncateStart(padded, width) : truncate(padded, width);
return index === 0 ? truncated : COLUMN_SEPARATOR + truncated;
}));
}
toRows() {
return this.toCells().map(row => row.join(''));
}
toString() {
return this.toRows().join('\n');
}
}