@tqman/ink-table
Version:
A table component for Ink.
237 lines (236 loc) • 8.76 kB
JavaScript
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
import React from 'react';
import { Box, Text } from 'ink';
import { sha1 } from 'object-hash';
/* Table */
export class Table extends React.Component {
constructor() {
/* Config */
super(...arguments);
/* Rendering utilities */
// The top most line in the table.
this.header = row({
cell: this.getConfig().skeleton,
padding: this.getConfig().padding,
skeleton: {
component: this.getConfig().skeleton,
// chars
line: '─',
left: '┌',
right: '┐',
cross: '┬',
},
});
// The line with column names.
this.heading = row({
cell: this.getConfig().header,
padding: this.getConfig().padding,
skeleton: {
component: this.getConfig().skeleton,
// chars
line: ' ',
left: '│',
right: '│',
cross: '│',
},
});
// The line that separates rows.
this.separator = row({
cell: this.getConfig().skeleton,
padding: this.getConfig().padding,
skeleton: {
component: this.getConfig().skeleton,
// chars
line: '─',
left: '├',
right: '┤',
cross: '┼',
},
});
// The row with the data.
this.data = row({
cell: this.getConfig().cell,
padding: this.getConfig().padding,
skeleton: {
component: this.getConfig().skeleton,
// chars
line: ' ',
left: '│',
right: '│',
cross: '│',
},
});
// The bottom most line of the table.
this.footer = row({
cell: this.getConfig().skeleton,
padding: this.getConfig().padding,
skeleton: {
component: this.getConfig().skeleton,
// chars
line: '─',
left: '└',
right: '┘',
cross: '┴',
},
});
}
/**
* Merges provided configuration with defaults.
*/
getConfig() {
return {
data: this.props.data,
columns: this.props.columns || this.getDataKeys(),
headings: this.props.headings || {},
padding: this.props.padding || 1,
header: this.props.header || Header,
cell: this.props.cell || Cell,
skeleton: this.props.skeleton || Skeleton,
};
}
/**
* Gets all keyes used in data by traversing through the data.
*/
getDataKeys() {
let keys = new Set();
// Collect all the keys.
for (const data of this.props.data) {
for (const key in data) {
keys.add(key);
}
}
return Array.from(keys);
}
/**
* Calculates the width of each column by finding
* the longest value in a cell of a particular column.
*
* Returns a list of column names and their widths.
*/
getColumns() {
const { columns, padding } = this.getConfig();
const widths = columns.map((propsOrKey) => {
const props = typeof propsOrKey === 'object'
? propsOrKey
: { key: propsOrKey, align: 'left' };
const key = props.key;
const header = String(key).length;
/* Get the width of each cell in the column */
const data = this.props.data.map((data) => {
const value = data[key];
if (value == undefined || value == null)
return 0;
return String(value).length;
});
const width = Math.max(...data, header) + padding * 2;
/* Construct a cell */
return {
column: key,
width: width,
key: String(key),
align: props.align ?? 'left',
};
});
return widths;
}
/**
* Returns a (data) row representing the headings.
*/
getHeadings() {
const columns = this.getConfig().columns.map((c) => typeof c === 'object' ? c.key : c);
const headings = columns.reduce((acc, column) => ({ ...acc, [column]: this.getConfig().headings[column] ?? column }), {});
return headings;
}
/* Render */
render() {
/* Data */
const columns = this.getColumns();
const headings = this.getHeadings();
/**
* Render the table line by line.
*/
return (_jsxs(Box, { flexDirection: "column", children: [this.header({ key: 'header', columns, data: {} }), this.heading({ key: 'heading', columns, data: headings }), this.props.data.map((row, index) => {
// Calculate the hash of the row based on its value and position
const key = `row-${sha1(row)}-${index}`;
// Construct a row.
return (_jsxs(Box, { flexDirection: "column", children: [this.separator({ key: `separator-${key}`, columns, data: {} }), this.data({ key: `data-${key}`, columns, data: row })] }, key));
}), this.footer({ key: 'footer', columns, data: {} })] }));
}
}
/**
* Constructs a Row element from the configuration.
*/
function row(config) {
/* This is a component builder. We return a function. */
const skeleton = config.skeleton;
/* Row */
return (props) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(skeleton.component, { children: skeleton.left }), ...intersperse((i) => {
const key = `${props.key}-hseparator-${i}`;
// The horizontal separator.
return (_jsx(skeleton.component, { children: skeleton.cross }, key));
},
// Values.
props.columns.map((column, colI) => {
// content
const value = props.data[column.column];
if (value == undefined || value == null) {
const key = `${props.key}-empty-${column.key}`;
return (_jsx(config.cell, { column: colI, children: skeleton.line.repeat(column.width) }, key));
}
else {
const key = `${props.key}-cell-${column.key}`;
// margins
const spaces = column.width - String(value).length;
let ml;
let mr;
if (column.align === 'left') {
ml = config.padding;
mr = spaces - ml;
}
else if (column.align === 'center') {
ml = Math.floor(spaces / 2);
mr = Math.ceil(spaces / 2);
}
else {
mr = config.padding;
ml = spaces - mr;
}
return (
/* prettier-ignore */
_jsx(config.cell, { column: colI, children: `${skeleton.line.repeat(ml)}${String(value)}${skeleton.line.repeat(mr)}` }, key));
}
})), _jsx(skeleton.component, { children: skeleton.right })] }));
}
/**
* Renders the header of a table.
*/
export function Header(props) {
return (_jsx(Text, { bold: true, color: "blue", children: props.children }));
}
/**
* Renders a cell in the table.
*/
export function Cell(props) {
return _jsx(Text, { children: props.children });
}
/**
* Redners the scaffold of the table.
*/
export function Skeleton(props) {
return _jsx(Text, { bold: true, children: props.children });
}
/* Utility functions */
/**
* Intersperses a list of elements with another element.
*/
function intersperse(intersperser, elements) {
// Intersparse by reducing from left.
let interspersed = elements.reduce((acc, element, index) => {
// Only add element if it's the first one.
if (acc.length === 0)
return [element];
// Add the intersparser as well otherwise.
return [...acc, intersperser(index), element];
}, []);
return interspersed;
}