console-toolkit
Version:
Toolkit to produce a fancy console output (boxes, tables, charts, colors).
334 lines (288 loc) • 11.9 kB
JavaScript
import Box from '../box.js';
import Panel from '../panel.js';
import {draw as drawBorder} from './draw-borders.js';
import style, {RESET_STATE} from '../style.js';
const getCellAlign = (align, index) => (typeof align == 'string' && align[index] !== 'd' && align[index]) || '';
const ensureSize = (cellSize, cellLength, cellGap, pos, lineTheme, axis, lengths) => {
let available = (cellSize - 1) * cellGap;
for (let i = 0; i < cellSize; ++i) {
available += lengths[pos + i];
if (i + 1 === cellSize || !axis[pos + i]) continue;
available += lineTheme ? lineTheme['w_' + axis[pos + i]] : 1;
}
if (cellLength > available) {
const diff = cellLength - available,
perCell = Math.floor(diff / cellSize),
remainder = diff % cellSize;
if (perCell) {
for (let i = 0; i < cellSize; ++i) lengths[pos + i] += perCell;
}
if (remainder) {
for (let i = 0; i < remainder; ++i) ++lengths[pos + i];
}
}
};
const dataInstructions = 'rowFirst,rowLast,columnFirst,columnLast,data,rowOdd,rowEven,columnOdd,columnEven'.split(',');
const DIM_STATE = style.dim.getState();
export class Table {
constructor(data, lineTheme, options = {}) {
let {hAxis = '1', vAxis = '1', hAlign = [], vAlign = [], hMin = 0, vMin = 0, cellPadding = {}} = options;
this.height = data.length;
this.width = this.height ? data[0].length : 0;
this.data = data;
if (!Array.isArray(hAlign)) hAlign = new Array(this.width).fill(hAlign);
if (!Array.isArray(vAlign)) vAlign = new Array(this.height).fill(vAlign);
this.widths = Array.isArray(hMin) ? [...hMin] : new Array(this.width).fill(hMin);
this.heights = Array.isArray(vMin) ? [...vMin] : new Array(this.height).fill(vMin);
if (this.widths.length != this.width)
throw new Error('hMin should be equal to a row size, not ' + this.widths.length);
if (this.heights.length != this.height)
throw new Error('vMin should be equal to a column size, not ' + this.heights.length);
this.lineTheme = lineTheme;
this.skipList = [];
this.options = {hAxis, vAxis, hAlign, vAlign};
{
const {l = 1, r = 1, t = 0, b = 0} = cellPadding;
this.cellPadding = {l, r, t, b};
}
this.hAxis = Array.isArray(hAxis) ? hAxis : new Array(this.width + 1).fill(hAxis);
this.vAxis = Array.isArray(vAxis) ? vAxis : new Array(this.height + 1).fill(vAxis);
if (this.hAxis.length != this.width + 1)
throw new Error('hAxis should be a row size plus 1, not ' + this.hAxis.length);
if (this.vAxis.length != this.height + 1)
throw new Error('vAxis should be a column size plus 1, not ' + this.vAxis.length);
this.cells = data.map((row, i) => {
const result = new Array(this.width);
for (let j = 0; j < this.width; ++j) {
const data = row[j];
if (data === null || data === undefined) {
result[j] = null;
continue;
}
const isObject = data?.hasOwnProperty('value'),
value = isObject ? data.value : data,
align = (isObject && data.align) || '',
box = value instanceof Box ? value : Box.make(value, {symbol: ' ', align: getCellAlign(align, 0) || hAlign[j] || 'left'}),
width = box.width,
height = box.height,
cellWidth = isObject ? data.width || 1 : 1,
cellHeight = isObject ? data.height || 1 : 1;
result[j] = {data, value, box, width, height, cellWidth, cellHeight, align};
if (cellWidth < 2) this.widths[j] = Math.max(this.widths[j], width);
if (cellHeight < 2) this.heights[i] = Math.max(this.heights[i], height);
if (cellWidth > 1 || cellHeight > 1) {
this.skipList.push({x: 2 * j + 1, y: 2 * i + 1, width: 2 * cellWidth - 1, height: 2 * cellHeight - 1});
}
}
return result;
});
// merged cells
this.skipList.forEach(rect => {
const j = (rect.x - 1) >> 1,
i = (rect.y - 1) >> 1,
cell = this.cells[i][j];
if (cell.cellWidth > 1) {
ensureSize(
cell.cellWidth,
cell.width,
this.cellPadding.l + this.cellPadding.r,
j,
lineTheme,
this.hAxis,
this.widths
);
}
if (cell.cellHeight > 1) {
ensureSize(
cell.cellHeight,
cell.height,
this.cellPadding.t + this.cellPadding.b,
i,
null,
this.vAxis,
this.heights
);
}
});
}
// TODO: accept `states`, draw even/odd rows/columns, support bg on lines optionally
draw({lineState = DIM_STATE} = {}) {
// prepare axes
const hAxis = new Array(this.hAxis.length + this.widths.length),
hPadding = this.cellPadding.l + this.cellPadding.r;
for (let i = 0; i < this.widths.length; ++i) {
hAxis[2 * i] = this.hAxis[i];
hAxis[2 * i + 1] = this.widths[i] + hPadding;
}
hAxis[hAxis.length - 1] = this.hAxis[this.hAxis.length - 1];
const vAxis = new Array(this.vAxis.length + this.heights.length),
vPadding = this.cellPadding.t + this.cellPadding.b;
for (let i = 0; i < this.heights.length; ++i) {
vAxis[2 * i] = this.vAxis[i];
vAxis[2 * i + 1] = this.heights[i] + vPadding;
}
vAxis[vAxis.length - 1] = this.vAxis[this.vAxis.length - 1];
// draw table borders
const borderBox = drawBorder(this.lineTheme, hAxis, vAxis, {skip: this.skipList, symbol: '\x07'}),
panel = Panel.make(borderBox, '\x07');
panel.fillNonEmptyState(0, 0, panel.width, panel.height, {state: lineState});
// draw cells
let y = (vAxis[0] ? 1 : 0) + this.cellPadding.t;
for (let i = 0; i < this.height; ++i) {
let x = (this.lineTheme['w_' + hAxis[0]] || 0) + this.cellPadding.l;
for (let j = 0; j < this.width; ++j) {
const cell = this.cells[i][j];
if (cell && this.isVisible(j, i)) {
let diffX = this.widths[j] - cell.width + (cell.cellWidth - 1) * (this.cellPadding.l + this.cellPadding.r);
for (let k = 1; k < cell.cellWidth; ++k) {
diffX += this.widths[j + k] + (this.hAxis[j + k] ? this.lineTheme['w_' + this.hAxis[j + k]] : 0);
}
let diffY = this.heights[i] - cell.height + (cell.cellHeight - 1) * (this.cellPadding.t + this.cellPadding.b);
for (let k = 1; k < cell.cellHeight; ++k) {
diffY += this.heights[i + k] + (this.vAxis[i + k] ? 1 : 0);
}
const hAlign = getCellAlign(cell.align, 0) || this.options.hAlign[j] || 'left',
vAlign = getCellAlign(cell.align, 1) || this.options.vAlign[i] || 'top',
dx = hAlign === 'l' || hAlign == 'left' ? 0 : hAlign === 'r' || hAlign === 'right' ? diffX : diffX >> 1,
dy = vAlign === 't' || vAlign == 'top' ? 0 : vAlign === 'b' || vAlign === 'bottom' ? diffY : diffY >> 1;
panel.put(x + dx, y + dy, cell.box);
}
x += hAxis[2 * j + 1] + (this.lineTheme['w_' + hAxis[2 * j + 2]] || 0);
}
y += vAxis[2 * i + 1] + (vAxis[2 * i + 2] ? 1 : 0);
}
return panel;
}
toPanel(options) {
return this.draw(options);
}
toBox(options) {
return this.toPanel(options).toBox(options);
}
toStrings(options) {
return this.toBox(options).toStrings();
}
isVisible(x, y) {
const i = 2 * y + 1,
j = 2 * x + 1;
for (const rect of this.skipList) {
if (rect.x === j && rect.y === i) return true;
if (rect.x <= j && j < rect.x + rect.width && rect.y <= i && i < rect.y + rect.height) return false;
}
return true;
}
static generateAxes(
width,
height,
{
hTheme = '1',
vTheme = '1',
borderTop,
borderRight,
borderLeft,
borderBottom,
rowFirst,
rowLast,
columnFirst,
columnLast,
hDataSep,
vDataSep,
hAlignDefault = 'l',
hLeft = [],
hCenter = [],
hRight = [],
vAlignDefault = 't',
vTop = [],
vCenter = [],
vBottom = [],
hMinDefault = 0,
hMin = {},
vMinDefault = 0,
vMin = {}
}
) {
const hAxis = new Array(width + 1).fill(hTheme);
if (borderLeft !== undefined) hAxis[0] = borderLeft;
if (borderRight !== undefined) hAxis[width] = borderRight;
{
let dataFrom = 1,
dataTo = width - 1;
if (columnFirst !== undefined && dataFrom <= dataTo) hAxis[dataFrom++] = columnFirst;
if (columnLast !== undefined && dataFrom <= dataTo) hAxis[dataTo--] = columnLast;
if (vDataSep !== undefined) {
for (let i = dataFrom; i <= dataTo; ++i) hAxis[i] = vDataSep;
}
}
const vAxis = new Array(height + 1).fill(vTheme);
if (borderTop !== undefined) vAxis[0] = borderTop;
if (borderBottom !== undefined) vAxis[height] = borderBottom;
{
let dataFrom = 1,
dataTo = height - 1;
if (rowFirst !== undefined && dataFrom <= dataTo) vAxis[dataFrom++] = rowFirst;
if (rowLast !== undefined && dataFrom <= dataTo) vAxis[dataTo--] = rowLast;
if (hDataSep !== undefined) {
for (let i = dataFrom; i <= dataTo; ++i) vAxis[i] = hDataSep;
}
}
const hAlign = new Array(width).fill(hAlignDefault);
for (const i of hLeft) hAlign[i] = 'l';
for (const i of hCenter) hAlign[i] = 'c';
for (const i of hRight) hAlign[i] = 'r';
const vAlign = new Array(height).fill(vAlignDefault);
for (const i of vTop) vAlign[i] = 't';
for (const i of vCenter) vAlign[i] = 'c';
for (const i of vBottom) vAlign[i] = 'b';
const hMinArray = new Array(width).fill(hMinDefault);
for (const [i, value] of Object.entries(hMin)) hMinArray[i] = value;
const vMinArray = new Array(height).fill(vMinDefault);
for (const [i, value] of Object.entries(vMin)) vMinArray[i] = value;
return {hAxis, vAxis, hAlign, vAlign, hMin: hMinArray, vMin: vMinArray};
}
static processData(data, options) {
if (!options) return data;
const available = dataInstructions.filter(name => options[name] !== undefined);
if (!available.length) return data;
const height = data.length,
width = height && data[0].length,
states = {};
available.forEach(name => (states[name] = options[name] || RESET_STATE));
const rowOdd = states.rowOdd || states.data,
rowEven = states.rowEven || states.data,
columnOdd = states.columnOdd || states.data,
columnEven = states.columnEven || states.data;
return data.map((row, i) => {
return row.map((data, j) => {
if (data === null) return null;
const isObject = data?.hasOwnProperty('value');
let datum = isObject ? data.value : data;
if (j === 0 && states.columnFirst) datum = style.addState(states.columnFirst).text(datum);
if (j === width - 1 && states.columnLast) datum = style.addState(states.columnLast).text(datum);
if (i === 0 && states.rowFirst) datum = style.addState(states.rowFirst).text(datum);
if (i === height - 1 && states.rowLast) datum = style.addState(states.rowLast).text(datum);
if (columnOdd || columnEven) {
if (j % 2) {
if (columnOdd) datum = style.addState(columnOdd).text(datum);
} else {
if (columnEven) datum = style.addState(columnEven).text(datum);
}
}
if (rowOdd || rowEven) {
if (i % 2) {
if (rowOdd) datum = style.addState(rowOdd).text(datum);
} else {
if (rowEven) datum = style.addState(rowEven).text(datum);
}
}
return isObject ? {...data, value: datum} : datum;
});
});
}
static make(data, lineTheme, options, overrides) {
return new Table(Table.processData(data, options?.states), lineTheme, {
...(options && Table.generateAxes(data.length && data[0].length, data.length, options)),
...overrides
});
}
}
export default Table;