Created
May 23, 2024 16:26
-
-
Save shadeglare/1446dc73a0bad01b6d26671289954767 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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