Skip to content

Instantly share code, notes, and snippets.

@shadeglare
Created May 23, 2024 16:26
Show Gist options
  • Save shadeglare/1446dc73a0bad01b6d26671289954767 to your computer and use it in GitHub Desktop.
Save shadeglare/1446dc73a0bad01b6d26671289954767 to your computer and use it in GitHub Desktop.
module Repeater {
export function repeat(value: string, count: number) {
if (count < 0) throw new Error("Count must be a non negative number");
return value.repeat(count);
}
}
module WhiteSpace {
export function repeat(count: number) {
return Repeater.repeat(" ", count);
}
export const Single = repeat(1);
export const Double = repeat(2);
export const Triple = repeat(3);
}
module Border {
export const TopLeft = "┌";
export const TopCenter = "┬";
export const TopRight = "┐";
export const BottomLeft = "└";
export const BottomCenter = "┴";
export const BottomRight = "┘";
export const MiddleLeft = "├";
export const MiddleCenter = "┼";
export const MiddleRight = "┤";
export const Vertical = "│";
export const Horizontal = "─";
export const None = "";
}
module Line {
export function horizontal(length: number) {
return Repeater.repeat(Border.Horizontal, length);
}
export function vertical(length: number) {
return Repeater.repeat(Border.Vertical, length);
}
}
module BorderPicker {
export function forColumn(
width: number,
height: number,
isFirstCell: boolean,
isLastCell: boolean,
isEmptyTable: boolean,
) {
const topLeft = isFirstCell ? Border.TopLeft : Border.TopCenter;
const bottomLeft = isFirstCell ?
(isEmptyTable ? Border.BottomLeft : Border.MiddleLeft) :
(isEmptyTable ? Border.BottomCenter : Border.MiddleCenter);
const topRight = isLastCell ? Border.TopRight : Border.None;
const bottomRight = isLastCell ? (isEmptyTable ? Border.BottomRight : Border.MiddleRight) : Border.None;
const top = Line.horizontal(width);
const bottom = top;
const left = Line.vertical(height);
const right = isLastCell ? left : Border.None;
return [
[topLeft, top, topRight],
[left, right],
[bottomLeft, bottom, bottomRight]
];
}
export function forRow(
width: number,
height: number,
isFirstCell: boolean,
isLastCell: boolean,
isLastRow: boolean,
) {
const bottomLeft = isFirstCell ?
(isLastRow ? Border.BottomLeft : Border.MiddleLeft) :
(isLastRow ? Border.BottomCenter : Border.MiddleCenter);
const bottomRight = isLastCell ? (isLastRow ? Border.BottomRight : Border.MiddleRight) : Border.None;
const left = Line.vertical(height);
const right = isLastCell ? left : Border.None;
const bottom = Line.horizontal(width);
return [
[left, right],
[bottomLeft, bottom, bottomRight]
];
}
}
export enum Color {
Green,
Red,
Yellow,
White,
Gray,
}
module Dyer {
export function dyeBorders(borders: string[][], color?: Color) {
if (color != null) {
for (let i = 0; i < borders.length; i++) {
const line = borders[i];
for (let j = 0; j < line.length; j++) {
line[j] = dyeText(line[j], color)
}
}
}
return borders;
}
export function dyeText(value: string, color: Color) {
switch (color) {
case Color.Green:
return `\x1b[32m${value}\x1b[0m`;
case Color.Red:
return `\x1b[31m${value}\x1b[0m`;
case Color.Yellow:
return `\x1b[33m${value}\x1b[0m`;
case Color.Gray:
return `\x1b[90m${value}\x1b[0m`;
case Color.White:
default:
return value;
}
}
}
type RowProperties<T> = { [prop in keyof T]: string }
module RowProperties {
export function fromObject<T extends object>(value: T): RowProperties<T> {
return Object.keys(value).reduce((acc, key) => {
(<any>acc)[key] = `${(<any>value)[key]}`;
return acc;
}, {} as { [prop in keyof T]: string });
}
}
interface ColumnProperty<T> {
key: keyof T;
name: string;
minWidth?: number;
color?: Color;
}
module ColumnProperties {
export const ColumnPaddings = 2;
function fromObject<T extends object>(value: T): Required<ColumnProperty<T>>[] {
return Object.keys(value).map(key => ({
key,
name: key,
minWidth: key.length + ColumnPaddings,
color: Color.White,
} as Required<ColumnProperty<T>>));
}
function normalize<T>(columnProperty: ColumnProperty<T>): Required<ColumnProperty<T>> {
const { minWidth: minLength, name } = columnProperty;
const actualMinLength = name.length + ColumnPaddings;
columnProperty.minWidth = Math.max(minLength ?? 0, actualMinLength);
columnProperty.color = columnProperty.color ?? Color.White;
return columnProperty as Required<ColumnProperty<T>>;
}
export function unpack<T extends object>(object: T, columnProperties?: ColumnProperty<T>[]) {
return columnProperties ? columnProperties.map(normalize) : fromObject(object);
}
function widenByContent<T extends object>(
columnProperty: Required<ColumnProperty<T>>,
contentLength: number,
) {
const newMinLength = contentLength + ColumnPaddings;
columnProperty.minWidth = Math.max(columnProperty.minWidth, newMinLength);
}
export function widen<T extends object>(
columnProperties: Required<ColumnProperty<T>>[],
rowProperties: RowProperties<T>
) {
for (let columnProperty of columnProperties) {
const rowProperty = rowProperties[columnProperty.key];
widenByContent(columnProperty, rowProperty.length)
}
}
}
module Renderer {
function renderContent<T>(
value: string,
{ minWidth, color }: Required<ColumnProperty<T>>,
ignoreColor: boolean = false,
) {
const tailWidth = minWidth - value.length - ColumnProperties.ColumnPaddings;
const text = ignoreColor ? value : Dyer.dyeText(value, color);
return `${WhiteSpace.Single}${text}${WhiteSpace.repeat(tailWidth)}${WhiteSpace.Single}`;
}
function renderColumns<T>(
columnProperties: Required<ColumnProperty<T>>[],
isEmptyTable: boolean,
borderColor?: Color,
) {
return columnProperties.reduce((acc, cp, i, arr) => {
const isFirstCell = i === 0;
const isLastCell = i === arr.length - 1;
const content = renderContent(cp.name, cp, true);
const [
[topLeft, top, topRight],
[left, right],
[bottomLeft, bottom, bottomRight],
] = Dyer.dyeBorders(
BorderPicker.forColumn(cp.minWidth, 1, isFirstCell, isLastCell, isEmptyTable),
borderColor,
);
acc[0].push(topLeft, top, topRight);
acc[1].push(left, content, right);
acc[2].push(bottomLeft, bottom, bottomRight);
return acc;
}, [[], [], []] as string[][]).map(x => x.join(""));
}
function renderRow<T>(
rowProperties: RowProperties<T>,
columnProperties: Required<ColumnProperty<T>>[],
isLastRow: boolean,
borderColor?: Color,
) {
return columnProperties.reduce((acc, cp, i, arr) => {
const isFirstCell = i === 0;
const isLastCell = i === arr.length - 1;
const content = renderContent(rowProperties[cp.key], cp);
const [
[left, right],
[bottomLeft, bottom, bottomRight]
] = Dyer.dyeBorders(
BorderPicker.forRow(cp.minWidth, 1, isFirstCell, isLastCell, isLastRow),
borderColor,
);
acc[0].push(left, content, right);
acc[1].push(bottomLeft, bottom, bottomRight);
return acc;
}, [[], []] as string[][]).map(x => x.join(""));
}
function renderRows<T>(
rowPropertiesGroup: RowProperties<T>[],
columnProperties: Required<ColumnProperty<T>>[],
borderColor?: Color,
) {
return rowPropertiesGroup.flatMap((rp, i, arr) => {
const isLastRow = i === arr.length - 1;
return renderRow(rp, columnProperties, isLastRow, borderColor);
});
}
export function renderTable<T extends object>(
values: T[],
options?: {
columnProperties?: ColumnProperty<T>[],
borderColor?: Color;
},
) {
const columnProperties = ColumnProperties.unpack(values[0], options?.columnProperties);
const rowPropertiesGroup = values.map(value => {
const rowProps = RowProperties.fromObject(value);
ColumnProperties.widen(columnProperties, rowProps);
return rowProps;
});
const isEmptyTable = values.length === 0;
const columns = renderColumns(columnProperties, isEmptyTable, options?.borderColor);
const rows = renderRows(rowPropertiesGroup, columnProperties, options?.borderColor);
return [...columns, ...rows];
}
}
export function print<T extends object>(
values: T[],
options?: {
columnProperties?: ColumnProperty<T>[],
borderColor?: Color;
},
) {
const lines = Renderer.renderTable(values, options);
for (let line of lines) {
console.log(line);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment