Terminal table rendering library with cascading style system and composable architecture.
npm i styled-cli-table
Usage:
import { RenderedStyledTable } from 'styled-cli-table/module/precomposed/RenderedStyledTable.js';
import { border, single } from 'styled-cli-table/module/styles/border.js'; // for CommonJS usage replace 'module' to 'commonjs' in path
const data = [
['#', 'name', 'price', 'quantity', 'total'],
[1, 'apple', 2, 3, 6],
[2, 'banana', 1, 10, 10],
[3, 'lemon', 1.5, 3, 4.5]
];
const styles = {
...border(true), // expands to { borderLeft: true, borderRight: true, borderTop: true, borderBottom: true }
borderCharacters: single,
paddingLeft: 1, paddingRight: 1,
rows: { // rows styles
0: { // first row styles
align: 'center'
}
},
columns: { // columns styles
1: { // second column styles
minWidth: 6, maxWidth: 12 // width calculated by the content, but will be at least 6 and at most 12 characters
},
[-1]: { // last column styles
width: 9 // fixed width
}
// widths of the remaining columns are calculated by the content
}
};
const table = new RenderedStyledTable(data, styles);
console.log(table.toString()); // print all at once
// or print line by line
for (const line of table.render()) {
console.log(line);
}
Output:
┌───┬────────┬───────┬──────────┬─────────┐
│ # │ name │ price │ quantity │ total │
├───┼────────┼───────┼──────────┼─────────┤
│ 1 │ apple │ 2 │ 3 │ 6 │
├───┼────────┼───────┼──────────┼─────────┤
│ 2 │ banana │ 1 │ 10 │ 10 │
├───┼────────┼───────┼──────────┼─────────┤
│ 3 │ lemon │ 1.5 │ 3 │ 4.5 │
└───┴────────┴───────┴──────────┴─────────┘
Vertical styles work as well:
const data = [
['#', 'name', 'price', 'quantity', 'total'],
[1, ['apple', 'Granny Smith'] /* arrays force rows to grow by height */, 2, 3, 6],
[2, 'banana', 1, 10, 10],
[3, 'lemon', 1.5, 3, 4.5],
];
const styles = {
...border(true),
borderCharacters: single,
paddingLeft: 1, paddingRight: 1,
rows: {
0: {
align: 'center',
paddingTop: 1, paddingBottom: 1
},
2: {
height: 3, // fixed row height
verticalAlign: 'bottom'
}
}
};
Output:
┌───┬──────────────┬───────┬──────────┬───────┐
│ │ │ │ │ │
│ # │ name │ price │ quantity │ total │
│ │ │ │ │ │
├───┼──────────────┼───────┼──────────┼───────┤
│ 1 │ apple │ 2 │ 3 │ 6 │
│ │ Granny Smith │ │ │ │
├───┼──────────────┼───────┼──────────┼───────┤
│ │ │ │ │ │
│ │ │ │ │ │
│ 2 │ banana │ 1 │ 10 │ 10 │
├───┼──────────────┼───────┼──────────┼───────┤
│ 3 │ lemon │ 1.5 │ 3 │ 4.5 │
└───┴──────────────┴───────┴──────────┴───────┘
Custom borders:
const styles = {
borderCharacters: single,
paddingLeft: 1, paddingRight: 1,
rows: {
0: {
align: 'center',
borderTop: true, borderBottom: true
},
[-1]: {
borderBottom: true
}
},
columns() { // functional styles (return value applies to all columns)
return {
borderLeft: true, borderRight: true
};
}
};
Output:
┌───┬────────┬───────┬──────────┬───────┐
│ # │ name │ price │ quantity │ total │
├───┼────────┼───────┼──────────┼───────┤
│ 1 │ apple │ 2 │ 3 │ 6 │
│ 2 │ banana │ 1 │ 10 │ 10 │
│ 3 │ lemon │ 1.5 │ 3 │ 4.5 │
└───┴────────┴───────┴──────────┴───────┘
Conditional data rendering:
const styles = {
borderCharacters: single,
paddingLeft: 1, paddingRight: 1,
rows: {
0: {
borderTop: true, borderBottom: true
},
[-1]: {
borderBottom: true
}
},
columns() {
return {
borderLeft: true, borderRight: true
};
},
cells({ value, rowIndex }) {
const isNumber = typeof value === 'number';
return {
content: isNumber ? value : `'${value}'`, // non-numbers are quoted
align: rowIndex === 0 ? 'center' :
isNumber ? 'right' : 'left' // first row aligned to center, numbers to right, other to left
};
}
}
Output:
┌─────┬──────────┬─────────┬────────────┬─────────┐
│ '#' │ 'name' │ 'price' │ 'quantity' │ 'total' │
├─────┼──────────┼─────────┼────────────┼─────────┤
│ 1 │ 'apple' │ 2 │ 3 │ 6 │
│ 2 │ 'banana' │ 1 │ 10 │ 10 │
│ 3 │ 'lemon' │ 1.5 │ 3 │ 4.5 │
└─────┴──────────┴─────────┴────────────┴─────────┘
Terminal color styling supported by seperate class:
import { ColorRenderedStyledTable } from 'styled-cli-table/module/precomposed/ColorRenderedStyledTable.js';
import * as color from 'styled-cli-table/module/styles/color.js';
const styles = {
...border(true),
borderCharacters: single,
paddingLeft: 1, paddingRight: 1,
backgroundColor: color.bgGreen,
color: color.blue,
rows: {
0: {
align: 'center',
color: color.yellow
}
}
};
const table = new ColorRenderedStyledTable(data, styles);
Output:
┌───┬────────┬───────┬──────────┬───────┐ │ # │ name │ price │ quantity │ total │ ├───┼────────┼───────┼──────────┼───────┤ │ 1 │ apple │ 2 │ 3 │ 6 │ │ 2 │ banana │ 1 │ 10 │ 10 │ │ 3 │ lemon │ 1.5 │ 3 │ 4.5 │ └───┴────────┴───────┴──────────┴───────┘
The library consists of the following main parts:
Style
class that provides a cascading style system;StyledTable
class that allows to iterate through tabular data with applying cascading styles;printline
module that provides mutable string buffers, that are used by therenderers
;renderers
module consisting of mixin classes that implement various styles;styles
module which consists of styles helper functions and types;ComposableRenderedStyledTable
function that composesStyledTable
and variousrenderers
mixin classes to the final reusable class.
Styles are plain JavaScript objects consisting of keys that denote the names of styles and values that denote the corresponding values:
const styles = {
align: 'center',
width: 10,
borderTop: true
};
To create cascading styles the Style
class is used:
import { Style } from 'styled-cli-table/module/styledtable/Style.js';
const style = new Style();
style.push({ // initial styles
padLeft: 2,
padRight: 2,
align: 'center'
});
style.push({ // later styles take precedence
padRight: 4, // overrides previous 'padRight'
width: 10 // new style
});
const {
align, // 'center'
padLeft, // 2
padRight, // 4
width, // 10
borderBottom // undefined
} = style.pick(['align', 'padLeft', 'padRight', 'width', 'borderBottom']); // get multiple styles at once
const width = style.get('width'); // get single style
The StyledTable
class allows to iterate through tabular data (wich are two-dimensional arrays) while apply cascading styles.
There are four levels of cascade (from low priority to high): table, columns, rows and cells.
import { StyledTable } from 'styled-cli-table/module/styledtable/StyledTable.js';
const data = [
['#', 'name', 'price', 'quantity', 'total'],
[1, 'apple', 2, 3, 6],
[2, 'banana', 1, 10, 10],
[3, 'lemon', 1.5, 3, 4.5],
];
const styles = {
// table level styles
width: 10,
align: 'left',
columns: { // column level styles
0: { // first column
width: 15
},
[-1]: { // last column
width: 20
}
}
rows: { // row level styles
0: {
align: 'center'
}
},
cells: { // cell level styles
1: {
1: { // cell at data[1][1]
align: 'right',
width: 12
}
}
}
};
const table = new StyledTable(data, styles);
for (const row of table.rows()) {
for (const cell of row.cells()) {
const styles = cell.style.pick(['width', 'align']);
console.log(`Value ${cell.value} at [${cell.rowIndex}, ${cell.columnIndex}] has styles:`, styles);
}
}
The columns
, rows
and cells
properties of table styles can be the functions returning styles objects. This allows you to define conditional styles.
const styles = {
// table level styles
width: 10,
align: 'left',
columns({ columnIndex, reversedColumnIndex }) { // functional column styles
if (columnIndex === 0) // first column
return { width: 15 };
if (reversedColumnIndex === -1) // last column
return { width: 20 };
}
rows({ rowIndex }) { // functional row styles
if (rowIndex === 0) // first row
return { align: 'center' };
},
cells({ rowIndex, columnIndex }) { // functional cell styles
if (rowIndex === 1 && columnIndex === 1) // cell at data[1][1]
return { align: 'right', width: 12 };
}
};
The printlines are some sort of mutable strings with predefined length. printline
module provides two kind of printlines: PrintLine
which is basic printline and BreakPrintLine
, which adds another layer of line breaks (can be used to add terminal styles for example).
import { PrintLine } from 'styled-cli-table/module/printline/PrintLine.js';
import { BreakPrintLine } from 'styled-cli-table/module/printline/BreakPrintLine.js';
const line = new PrintLine(10, '.'); // width and space character
line.fill(5, 'bb');
line.fill(0, 'aaa');
line.fill(11, 'ccc'); // out of range
console.log(line.join()); // 'aaa..bb...'
const line2 = new BreakPrintLine(10, '.');
line2.fill(0, 'aaa');
line2.insertBreak(5, '<i>');
line2.insertBreak(7, '</i>');
line2.fill(5, 'bb');
console.log(line2.join()); // 'aaa..<i>bb</i>...'
The AbstractPrintLineBuffer
class allows to allocate some number of underlying printlines, fill them and consume filled lines on demand. There are two predifined buffers: PrintLineBuffer
and BreakPrintLineBuffer
.
import { PrintLineBuffer } from 'styled-cli-table/module/printline/PrintLineBuffer.js';
const buffer = new PrintLineBuffer('.');
buffer.push(11, 2); // allocates 2 lines of width 11
buffer.fill(0, 0, 'aaa'); // filling first line
buffer.fill(8, 0, 'bbb');
buffer.fill(0, 1, 'xxx'); // filling second line
buffer.fill(8, 2, 'zzz'); // out of range
for (const line of buffer.shift(2)) // consumes first two lines of buffer
console.log(line); // 'aaa.....bbb' and 'xxx........'
// buffer is empty now, you can allocate new lines
The renderer is a class that have render
method that accepts StyledTable
and generates its string representation line by line.
The core class of the library is AbstractBufferedRenderer
, which provides two-pass buffered rendering: on the first pass the necessary styles are computed (e.g. column widths and row heights), on the second pass the table data are rendered using computed styles and printline buffer.
Here is a simplified version of the render
method of class AbstractBufferedRenderer
:
function *render(this: AbstractBufferedRenderer, styledTable: StyledTable) {
const computedStyles: Styles = {};
this.initComputedStyles(computedStyles); // you can init your styles here
// first pass, computing styles
for (const row of styledTable.rows()) {
for (const cell of row.cells()) {
this.computeStyles(cell, computedStyles); // compute your styles here
}
}
// second pass, renedring table line by line using buffer
const buffer = this.createBuffer(styledTable.style.get('space'));
const rowWidth = this.getRowWidth(computedStyles); // gets the whole table width
for (const row of styledTable.rows()) {
const rowHeight = this.getRowHeight(row, computedStyles); // gets current row height
const y = buffer.height;
buffer.push(rowWidth, rowHeight); // roughly allocates rowWidth * rowHeight characters to render current row
const height = rowHeight;
let x = 0;
for (const cell of row.cells()) {
const width = this.getCellWidth(cell, computedStyles); // gets current cell width
const content = this.getContent(cell); // gets the content to render (by default, cell's 'value' property or cell.style's 'content' property)
this.fillBlock(buffer, x, y, Array.isArray(content) ? content : [content], width, height, cell, computedStyles); // use this method to process the current cell's block
x += width;
}
yield* buffer.shift(this.getRowShift(row, computedStyles)); // shifts buffer by specified number of line and yields them to user
}
}
function fillBlock(this: AbstractBufferedRenderer, buffer: TBuffer, x: number, y: number, content: any[], width: number, height: number, cell: StyledCell, computedStyles: ComputedCellStyles) {
for (let i = 0; i < height; i++)
this.fillLine(buffer, x, y++, String(content[i]), width, cell, computedStyles); // use this method to process current cell's content
}
To implement concrete styles or features, mixin functions are used. There are several predefined mixins provided by the library.
Accepts the subclass of AbstractPrintLineBuffer
(e.g. PrintLineBuffer
or BreakPrintLineBuffer
) and returns concrete subclass of AbstractBufferedRenderer
parameterized with this buffer. This is the base mixin for all library mixins.
Computes the height and width of cells depending on the content. You can specify a fixed width or height using width
and height
styles respectively. You can specify a fixed range of width or height using styles minWidth
, maxWidth
, minHeight
and maxHeight
respectively. Usually this mixin should be used immediately after GenericBufferedRenderer
.
Adds horizontal and vertical paddings. You can set paddings explicitly by setting paddingTop
, paddingRight
, paddingBottom
or paddingLeft
properties, or by using a helper padding
function:
import { padding } from 'styled-cli-table/module/styles/padding.js';
const styles = {
...padding(0, 1) // expands to { paddingTop: 0, paddingRight: 1, paddingBottom: 0, paddingLeft: 1 }
};
Aligns the content horizontally and vertically. To align the content horizontally set align
property to left
, center
or right
. To align the content vertically set verticallAlign
property to top
, middle
or bottom
.
Renders borders around cells. You can set borders explicitly by setting borderTop
, borderRight
, borderBottom
or borderLeft
properties, or by using a helper border
function. It is also necessary to explicitly set the border characters by specifiyng borderCharacters
proprty, otherwise the borders will not be displayed. The library provides two character sets by default: single
and double
:
import { border, single } from 'styled-cli-table/module/styles/border.js';
const styles = {
...border(true), // expands to { borderTop: true, borderRight: true, borderBottom: true, borderLeft: true }
borderCharacters: single
};
You can create your own sets by using the borderCharacters
function:
import { border, borderCharacters } from 'styled-cli-table/module/styles/border.js';
const styles = {
...border(true),
borderCharacters: borderCharacters(
'═', '═', '─',
'║', '║', '│',
'╔', '╤', '╗',
'╟', '┼', '╢',
'╚', '╧', '╝',
)
};
Allows you to apply terminal styles for the entire cell (BackgroundColorRenderer
) or for the content only (ColorRenderer
). To set whole cell style use backgroundColor
property. To set only content style use color
property. Predefined terminal styles can be found inside styles/color
module:
import * as color from 'styled-cli-table/module/styles/color.js';
const styles = {
backgroundColor: color.bgYellow,
color: color.black
};
These mixins should be use with BreakPrintLineBuffer
.
You can compose renderers by chaining mixin functions and providing printline buffer to break the chain, e.g.:
import { PrintLineBuffer } from 'styled-cli-table/module/printline/PrintLineBuffer.js';
import { BorderRenderer, FlexSizeRenderer, GenericBufferedRenderer } from 'styled-cli-table/module/renderers/index.js';
import { StyledTable } from 'styled-cli-table/module/styledtable/StyledTable.js';
const CustomRenderer = BorderRenderer(FlexSizeRenderer(GenericBufferedRenderer(PrintLineBuffer)));
const renderer = new CustomRenderer();
const table = new StyledTable(data, styles);
console.log(renderer.toString(table));
To simplify creation and usage of renderers, GenericRenderedStyledTable
function is provided. It accepts the base class as first parameter, and one or more mixins functions as rest, chains them internally and returns new class that can be used to create and render tabular data:
import { PrintLineBuffer } from 'styled-cli-table/module/printline/PrintLineBuffer.js';
import { BorderRenderer, FlexSizeRenderer, GenericBufferedRenderer } from 'styled-cli-table/module/renderers/index.js';
import { GenericRenderedStyledTable } from 'styled-cli-table/module/composable/ComposableRenderedStyledTable.js';
const CustomStyledTable = GenericRenderedStyledTable(
PrintLineBuffer,
GenericBufferedRenderer,
FlexSizeRenderer,
BorderRenderer
);
const table = new CustomStyledTable(data, styles);
console.log(table.toString());
import { PrintLineBuffer } from 'styled-cli-table/module/printline/PrintLineBuffer.js';
import { BorderRenderer, PaddingRenderer, AlignRenderer, FlexSizeRenderer, GenericBufferedRenderer } from 'styled-cli-table/module/renderers/index.js';
import { ComposableRenderedStyledTable } from 'styled-cli-table/module/composable/ComposableRenderedStyledTable.js';
import { border, single } from 'styled-cli-table/module/styles/border.js';
function PrettyCropRenderer(BufferedRenderer) {
return class extends BufferedRenderer {
fillLine(buffer, x, y, content, width, cell, computedStyles) {
super.fillLine(buffer, x, y, crop(content, width, cell.style.get('crop')), width, cell, computedStyles);
}
};
}
function crop(content, width, cropString = '') {
return content.length > width ?
content.substring(0, width - cropString.length) + cropString :
content;
}
const CustomStyledTable = ComposableRenderedStyledTable(
PrintLineBuffer,
GenericBufferedRenderer,
FlexSizeRenderer,
AlignRenderer,
PrettyCropRenderer,
PaddingRenderer,
BorderRenderer
);
const data = [
['#', 'name', 'price', 'quantity', 'total'],
[1, 'apple', 2, 3, 6],
[2, 'banana', 1, 10, 10],
[3, 'strawberry', 3, 3, 9]
];
const styles = {
...border(true),
borderCharacters: single,
paddingLeft: 1, paddingRight: 1,
crop: '..',
rows: {
0: {
align: 'center'
}
},
columns: {
1: {
width: 8
}
}
};
const table = new CustomStyledTable(data, styles);
console.log(table.toString());
Output:
┌───┬────────┬───────┬──────────┬───────┐
│ # │ name │ price │ quantity │ total │
├───┼────────┼───────┼──────────┼───────┤
│ 1 │ apple │ 2 │ 3 │ 6 │
├───┼────────┼───────┼──────────┼───────┤
│ 2 │ banana │ 1 │ 10 │ 10 │
├───┼────────┼───────┼──────────┼───────┤
│ 3 │ stra.. │ 3 │ 3 │ 9 │
└───┴────────┴───────┴──────────┴───────┘
The same in Typescript:
import { PrintLineBuffer } from 'styled-cli-table/module/printline/PrintLineBuffer';
import { BorderRenderer, PaddingRenderer, AlignRenderer, FlexSizeRenderer, GenericBufferedRenderer } from 'styled-cli-table/module/renderers/index';
import { ComposableRenderedStyledTable } from 'styled-cli-table/module/composable/ComposableRenderedStyledTable';
import { border, single } from 'styled-cli-table/module/styles/index';
import type { AbstractPrintLineBuffer } from 'styled-cli-table/module/printline/AbstractPrintLineBuffer';
import type { ComputedCellStyles } from 'styled-cli-table/module/renderers/AbstractBufferedRenderer';
import type { StyledCell } from 'styled-cli-table/module/styledtable/StyledTable';
import type { Constructor } from 'styled-cli-table/module/util/Constructor';
function PrettyCropRenderer<TBuffer extends AbstractPrintLineBuffer>(BufferedRenderer: Constructor<FlexSizeRenderer<TBuffer>>) {
return class extends BufferedRenderer {
fillLine(buffer: TBuffer, x: number, y: number, content: string, width: number, cell: StyledCell, computedStyles: ComputedCellStyles) {
super.fillLine(buffer, x, y, crop(content, width, cell.style.get('crop')), width, cell, computedStyles);
}
};
}
function crop(content: string, width: number, cropString = '') {
return content.length > width ?
content.substring(0, width - cropString.length) + cropString :
content;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>styled-cli-table example</title>
</head>
<body>
<pre id="output"></pre>
<script type="module">
import { RenderedStyledTable } from 'https://unpkg.com/styled-cli-table/module/precomposed/RenderedStyledTable.js';
import { border, single } from 'https://unpkg.com/styled-cli-table/module/styles/border.js';
const data = [
['#', 'name', 'price', 'quantity', 'total'],
[1, 'apple', 2, 3, 6],
[2, 'banana', 1, 10, 10],
[3, 'lemon', 1.5, 3, 4.5]
];
const styles = {
...border(true),
borderCharacters: single,
paddingLeft: 1, paddingRight: 1,
rows: {
0: {
align: 'center'
}
},
columns: {
1: {
minWidth: 6, maxWidth: 12
},
[-1]: {
width: 9
}
}
};
const table = new RenderedStyledTable(data, styles);
const output = document.querySelector('#output');
for (const line of table.render()) {
output.appendChild(document.createTextNode(line));
output.appendChild(document.createElement('br'));
console.log(line); // echoing
}
</script>
</body>
</html>