diff --git a/command/upgrade/provider.ts b/command/upgrade/provider.ts index 38e09d4f..e7f09192 100644 --- a/command/upgrade/provider.ts +++ b/command/upgrade/provider.ts @@ -154,7 +154,7 @@ export abstract class Provider { ); if (versions.length > this.maxListSize) { - const table = new Table().indent(indent); + const table = new Table>().indent(indent); const rowSize = Math.ceil(versions.length / maxCols); const colSize = Math.min(versions.length, maxCols); let versionIndex = 0; diff --git a/examples/table/basic_usage.ts b/examples/table/basic_usage.ts index 80b09cff..c115590d 100755 --- a/examples/table/basic_usage.ts +++ b/examples/table/basic_usage.ts @@ -2,7 +2,7 @@ import { Table } from "../../table/table.ts"; -const table: Table = new Table( +const table = new Table( ["Baxter Herman", "Oct 1, 2020", "Harderwijk", "Slovenia"], ["Jescie Wolfe", "Dec 4, 2020", "Alto Hospicio", "Japan"], ["Allegra Cleveland", "Apr 16, 2020", "Avernas-le-Bauduin", "Samoa"], diff --git a/examples/table/datatable.ts b/examples/table/datatable.ts new file mode 100755 index 00000000..a4f76c5c --- /dev/null +++ b/examples/table/datatable.ts @@ -0,0 +1,40 @@ +#!/usr/bin/env -S deno run + +import { colors } from "../../ansi/colors.ts"; +import { Table } from "../../table/table.ts"; + +new Table() + .header(["Name", "Age", "Email"]) + .body([ + { + firstName: "Gino", + lastName: "Aicheson", + age: 21, + email: "gaicheson0@nydailynews.com", + }, + { + firstName: "Godfry", + lastName: "Pedycan", + age: 33, + email: "gpedycan1@state.gov", + }, + { + firstName: "Loni", + lastName: "Miller", + age: 24, + email: "lmiller2@chron.com", + }, + ]) + .columns([{ + value: ({ firstName, lastName }) => + colors.brightBlue.bold(`${firstName} ${lastName}`), + }, { + align: "right", + value: ({ age }) => colors.yellow(age.toString()), + }, { + minWidth: 20, + value: ({ email }) => colors.cyan.italic(email), + }]) + .headerValue((value) => colors.bold(value)) + .border() + .render(); diff --git a/examples/table/random_table_demo.ts b/examples/table/random_table_demo.ts index f5c6071c..0f105cbe 100755 --- a/examples/table/random_table_demo.ts +++ b/examples/table/random_table_demo.ts @@ -27,13 +27,13 @@ Deno.addSignalListener("SIGINT", () => { loop(); function loop() { - const table: Table = createTable(); + const table = createTable(); tty.eraseScreen.cursorTo(0, 0); table.render(); setTimeout(loop, 1000); } -function createTable(): Table { +function createTable() { return new Table() .header( ["ID", "First Name", "Last Name", "Email", "Gender", "IP-Address"].map( diff --git a/examples/table/using_as_array.ts b/examples/table/using_as_array.ts index 2eb0d158..2fa43624 100755 --- a/examples/table/using_as_array.ts +++ b/examples/table/using_as_array.ts @@ -2,7 +2,7 @@ import { Table } from "../../table/table.ts"; -const table: Table = Table.from([ +const table = Table.from([ ["Baxter Herman", "Oct 1, 2020", "Harderwijk", "Slovenia"], ["Jescie Wolfe", "Dec 4, 2020", "Alto Hospicio", "Japan"], ["Allegra Cleveland", "Apr 16, 2020", "Avernas-le-Bauduin", "Samoa"], diff --git a/table/_layout.ts b/table/_layout.ts index 998b4d28..3d0a4bc4 100644 --- a/table/_layout.ts +++ b/table/_layout.ts @@ -1,7 +1,13 @@ +import { + Cell, + type CellType, + type CellValue, + type Direction, + type ValueParser, +} from "./cell.ts"; import type { Column } from "./column.ts"; -import { Cell, CellType, Direction } from "./cell.ts"; import { consumeChars, consumeWords } from "./consume_words.ts"; -import { Row, RowType } from "./row.ts"; +import { GetRowInnerValue, Row, type RowType } from "./row.ts"; import type { BorderOptions, Table, TableSettings } from "./table.ts"; import { getUnclosedAnsiRuns, longest, strLength } from "./_utils.ts"; @@ -17,15 +23,18 @@ interface RenderSettings { } /** Table layout renderer. */ -export class TableLayout { +export class TableLayout< + TRow extends RowType, + THeaderRow extends RowType, +> { /** * Table layout constructor. * @param table Table instance. * @param options Render options. */ public constructor( - private table: Table, - private options: TableSettings, + private table: Table, + private options: TableSettings, ) {} /** Generate table string. */ @@ -53,7 +62,10 @@ export class TableLayout { const rows = this.#getRows(); - const columns: number = Math.max(...rows.map((row) => row.length)); + const columns: number = Math.max( + this.options.columns.length, + ...rows.map((row) => row.length), + ); for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { const row = rows[rowIndex]; const length: number = row.length; @@ -100,9 +112,11 @@ export class TableLayout { } #getRows(): Array> { - const header: Row | undefined = this.table.getHeader(); + const header: Row | undefined = this.table + .getHeader(); const rows = header ? [header, ...this.table] : this.table.slice(); const hasSpan = rows.some((row) => + Array.isArray(row) && row.some((cell) => cell instanceof Cell && (cell.getColSpan() > 1 || cell.getRowSpan() > 1) ) @@ -111,26 +125,37 @@ export class TableLayout { if (hasSpan) { return this.spanRows(rows); } + const isDataTable = this.isDataTable(); return rows.map((row, rowIndex) => { - const newRow = this.createRow(row); - for (let colIndex = 0; colIndex < row.length; colIndex++) { + const newRow = Row.from(row) as Row; + const dataCell = isDataTable ? Cell.from(newRow[0]) : null; + const length = isDataTable ? this.options.columns.length : newRow.length; + + for (let colIndex = 0; colIndex < length; colIndex++) { newRow[colIndex] = this.createCell( - row[colIndex], + newRow[colIndex] ?? dataCell, newRow, rowIndex, colIndex, ); } + return newRow; }); } + private isDataTable(): boolean { + const row = this.table[0]; + return !!row && !(row instanceof Row || Array.isArray(row)) && + typeof row === "object"; + } + /** * Fills rows and cols by specified row/col span with a reference of the * original cell. */ - protected spanRows(rows: Array) { + protected spanRows(rows: Array>) { const rowSpan: Array = []; let colSpan = 1; let rowIndex = -1; @@ -140,7 +165,7 @@ export class TableLayout { if (rowIndex === rows.length && rowSpan.every((span) => span === 1)) { break; } - const row = rows[rowIndex] = this.createRow(rows[rowIndex] || []); + const row = rows[rowIndex] = Row.from(rows[rowIndex] || []); let colIndex = -1; while (true) { @@ -166,10 +191,11 @@ export class TableLayout { if (rowSpan[colIndex] > 1) { rowSpan[colIndex]--; - rows[rowIndex].splice( + const prevRow = rows[rowIndex - 1] as Row; + row.splice( colIndex, this.getDeleteCount(rows, rowIndex, colIndex), - rows[rowIndex - 1][colIndex], + prevRow[colIndex], ); continue; @@ -191,51 +217,70 @@ export class TableLayout { } protected getDeleteCount( - rows: Array>, + rows: Array>, rowIndex: number, colIndex: number, ) { - return colIndex <= rows[rowIndex].length - 1 && - typeof rows[rowIndex][colIndex] === "undefined" + const row: RowType = rows[rowIndex]; + return Array.isArray(row) && colIndex <= row.length - 1 && + typeof row[colIndex] === "undefined" ? 1 : 0; } - /** - * Create a new row from existing row or cell array. - * @param row Original row. - */ - protected createRow(row: RowType): Row { - return Row.from(row) - .border(this.table.getBorder(), false) - .align(this.table.getAlign(), false) as Row; - } - /** * Create a new cell from existing cell or cell value. * - * @param cell Original cell. + * @param value Original cell. * @param row Parent row. * @param rowIndex The row index of the cell. * @param colIndex The column index of the cell. */ protected createCell( - cell: CellType | null | undefined, - row: Row, + value: CellType, + row: Row, rowIndex: number, colIndex: number, ): Cell { - const column: Column | undefined = this.options.columns.at(colIndex); + const column: + | Column, GetRowInnerValue> + | undefined = this.options.columns + .at(colIndex); const isHeaderRow = this.isHeaderRow(rowIndex); - return Cell.from(cell ?? "") - .border( - (isHeaderRow ? null : column?.getBorder()) ?? row.getBorder(), - false, - ) - .align( - (isHeaderRow ? null : column?.getAlign()) ?? row.getAlign(), - false, + const cell = Cell.from(value ?? "") as Cell; + + if (typeof cell.getBorder() === "undefined") { + cell.border( + row.getBorder() ?? (isHeaderRow ? null : column?.getBorder()) ?? + this.table.getBorder() ?? + false, + ); + } + + if (!cell.getAlign()) { + cell.align( + row.getAlign() ?? (isHeaderRow ? null : column?.getAlign()) ?? + this.table.getAlign() ?? "left", ); + } + + const cellValueParser = + (cell.getValueParser() ?? row.getCellValueParser() ?? + ( + isHeaderRow + ? column?.getHeaderValueParser() + : column?.getCellValueParser() + ) ?? + (isHeaderRow + ? this.table.getHeaderValueParser() + : this.table.getCellValueParser())) as ValueParser; + + if (cellValueParser) { + cell.value(cellValueParser); + cell.setValue(cellValueParser(cell.getValue())); + } + + return cell; } private isHeaderRow(rowIndex: number) { @@ -416,11 +461,12 @@ export class TableLayout { cell: Cell, maxLength: number, ): { current: string; next: string } { + const value = cell.toString(); const length: number = Math.min( maxLength, - strLength(cell.toString()), + strLength(value), ); - let words: string = consumeWords(length, cell.toString()); + let words: string = consumeWords(length, value); // break word if word is longer than max length const breakWord = strLength(words) > length; @@ -430,7 +476,7 @@ export class TableLayout { // get next content and remove leading space if breakWord is not true // calculate from words.length _before_ any handling of unclosed ANSI codes - const next = cell.toString().slice(words.length + (breakWord ? 0 : 1)); + const next = value.slice(words.length + (breakWord ? 0 : 1)); words = cell.unclosedAnsiRuns + words; @@ -442,7 +488,7 @@ export class TableLayout { const fillLength = maxLength - strLength(words); // Align content - const align: Direction = cell.getAlign(); + const align: Direction = cell.getAlign() ?? "left"; let current: string; if (fillLength === 0) { current = words; diff --git a/table/cell.ts b/table/cell.ts index 206fc938..5e6b81b1 100644 --- a/table/cell.ts +++ b/table/cell.ts @@ -1,14 +1,28 @@ /** Allowed cell value type. */ -export type CellValue = number | string; +export type CellValue = unknown; +// export type CellValue = number | string | Record | undefined | null; -/** Allowed cell type. */ -export type CellType = CellValue | Cell; +/** Cell type. */ +export type CellType = + | Cell + | TValue; + +export type GetCellValue = TCell extends infer TCell + ? TCell extends Cell ? Value + : TCell + : never; /** Cell alignment direction. */ export type Direction = "left" | "right" | "center"; +export type ValueParserResult = string | number | undefined | null | void; + +export type ValueParser = ( + value: TValue, +) => ValueParserResult; + /** Cell options. */ -interface CellOptions { +export interface CellOptions { /** Enable/disable cell border. */ border?: boolean; /** Set coll span. */ @@ -17,6 +31,8 @@ interface CellOptions { rowSpan?: number; /** Cell cell alignment direction. */ align?: Direction; + // value?: ValueParser; + value?(value: TValue): ValueParserResult; /** * Any unterminated ANSI formatting overflowed from previous lines of a * multi-line cell. @@ -40,8 +56,9 @@ interface CellOptions { * .render(); * ``` */ -export class Cell { - protected options: CellOptions = {}; +/** Cell representation. */ +export class Cell { + protected options: CellOptions = {}; /** Get cell length. */ public get length(): number { @@ -65,32 +82,35 @@ export class Cell { * * @param value Cell or cell value. */ - public static from(value: CellType): Cell { - let cell: Cell; + public static from( + value: CellType, + ): Cell { if (value instanceof Cell) { - cell = new this(value.getValue()); + const cell = new this(value.getValue()); cell.options = { ...value.options }; - } else { - cell = new this(value); + return cell; } - return cell; + + return new this(value); } /** * Cell constructor. * - * @param value Cell value. + * @param cellValue Cell value. */ - public constructor(private value: CellValue) {} + public constructor( + private cellValue?: TValue | undefined | null, + ) {} /** Get cell string value. */ public toString(): string { - return this.value.toString(); + return this.cellValue?.toString() ?? ""; } /** Get cell value. */ - public getValue(): CellValue { - return this.value; + public getValue(): TValue | undefined | null { + return this.cellValue; } /** @@ -98,8 +118,8 @@ export class Cell { * * @param value Cell or cell value. */ - public setValue(value: CellValue): this { - this.value = value; + public setValue(value: TValue | undefined | null): this { + this.cellValue = value; return this; } @@ -108,8 +128,12 @@ export class Cell { * * @param value Cell or cell value. */ - public clone(value?: CellValue): Cell { - return Cell.from(value ?? this); + public clone( + value?: TCloneValue, + ): Cell { + const cell = new Cell(value ?? this.getValue() as TCloneValue); + cell.options = { ...this.options } as CellOptions; + return cell; } /** @@ -200,13 +224,22 @@ export class Cell { return this; } + /** + * Register cell value parser. + * @param fn Value parser callback function. + */ + public value(fn: ValueParser): this { + this.options.value = fn; + return this; + } + /** * Getter: */ /** Check if cell has border. */ - public getBorder(): boolean { - return this.options.border === true; + public getBorder(): boolean | undefined { + return this.options.border; } /** Get col span. */ @@ -223,9 +256,14 @@ export class Cell { : 1; } - /** Get row span. */ - public getAlign(): Direction { - return this.options.align ?? "left"; + /** Get cell alignment. */ + public getAlign(): Direction | undefined { + return this.options.align; + } + + /** Get value parser. */ + public getValueParser(): ValueParser | undefined { + return this.options.value; } } diff --git a/table/column.ts b/table/column.ts index 062a817f..60284f37 100644 --- a/table/column.ts +++ b/table/column.ts @@ -1,7 +1,10 @@ -import { Direction } from "./cell.ts"; +import { CellValue, Direction, ValueParser } from "./cell.ts"; /** Column options. */ -export interface ColumnOptions { +export interface ColumnOptions< + TValue extends CellValue, + THeaderValue extends CellValue = TValue, +> { /** Enable/disable cell border. */ border?: boolean; /** Cell cell alignment direction. */ @@ -12,6 +15,8 @@ export interface ColumnOptions { maxWidth?: number; /** Set cell padding. */ padding?: number; + headerValue?: ValueParser; + value?: ValueParser; } /** @@ -23,62 +28,111 @@ export interface ColumnOptions { * import { Column, Table } from "./mod.ts"; * * new Table() + * .header(["One", "Two"]) * .body([ * ["Foo", "bar"], * ["Beep", "Boop"], * ]) - * .column(0, new Column().border()) + * .column(0, new Column({ + * border: true, + * })) * .render(); * ``` */ -export class Column { +export class Column< + TValue extends CellValue, + THeaderValue extends CellValue = TValue, +> { /** - * Create a new cell from column options or an existing column. + * Create a new column from column options or an existing column. * @param options */ - static from(options: ColumnOptions | Column): Column { + static from< + TValue extends CellValue, + THeaderValue extends CellValue, + >( + options: ColumnOptions | Column, + ): Column { const opts = options instanceof Column ? options.opts : options; - return new Column().options(opts); + return new Column().options(opts); } - protected opts: ColumnOptions = {}; + constructor( + protected opts: ColumnOptions = {}, + ) {} - /** Set column options. */ - options(options: ColumnOptions): this { + /** + * Set column options. + * @param options Column options. + */ + options(options: ColumnOptions): this { Object.assign(this.opts, options); return this; } - /** Set min column width. */ + /** + * Set min column width. + * @param width Min column width. + */ minWidth(width: number): this { this.opts.minWidth = width; return this; } - /** Set max column width. */ + /** + * Set max column width. + * @param width Max column width. + */ maxWidth(width: number): this { this.opts.maxWidth = width; return this; } - /** Set column border. */ + /** + * Set column border. + * @param border + */ border(border = true): this { this.opts.border = border; return this; } - /** Set column padding. */ + /** + * Set column left and right padding. + * @param padding Padding. + */ padding(padding: number): this { this.opts.padding = padding; return this; } - /** Set column alignment. */ + /** + * Set column alignment. + * @param direction Column alignment. + */ align(direction: Direction): this { this.opts.align = direction; return this; } + /** + * Register header value parser. + * @param fn Value parser callback function. + */ + headerValue(fn: ValueParser): this { + this.opts.headerValue = fn; + return this; + } + + /** + * Register cell value parser. + * @param fn Value parser callback function. + */ + value(fn: ValueParser): this { + this.opts.value = fn; + return this; + } + /** Get min column width. */ getMinWidth(): number | undefined { return this.opts.minWidth; @@ -103,4 +157,14 @@ export class Column { getAlign(): Direction | undefined { return this.opts.align; } + + /** Get header value parser. */ + getHeaderValueParser(): ValueParser | undefined { + return this.opts.headerValue; + } + + /** Get value parser. */ + getCellValueParser(): ValueParser | undefined { + return this.opts.value; + } } diff --git a/table/row.ts b/table/row.ts index e7033069..6e94136f 100644 --- a/table/row.ts +++ b/table/row.ts @@ -1,20 +1,46 @@ -import { Cell, CellType, Direction } from "./cell.ts"; +import { + Cell, + CellType, + CellValue, + Direction, + GetCellValue, + ValueParser, +} from "./cell.ts"; -/** Allowed row type. */ -export type RowType< - T extends CellType | undefined = CellType | undefined, -> = - | Array - | Row; +/** Row type. */ +export type RowType = + | Row + | ReadonlyArray + | TCell; -/** Json row. */ -export type DataRow = Record; +export type GetRowInnerValue> = TRow extends + infer TRow + ? TRow extends ReadonlyArray + ? GetCellValue + : TRow + : never; + +export type GetRowValue> = TRow extends + infer TRow ? TRow extends ReadonlyArray ? Value + : TRow + : never; + +export type UnwrapRow> = TRow extends infer TRow + ? TRow extends Row ? Value + : TRow + : never; + +/** @deprecated Use `Record` instead. */ +export type DataRow = Record; /** Row options. */ -interface RowOptions { +export interface RowOptions< + TValue extends CellValue, +> { indent?: number; border?: boolean; align?: Direction; + value?: ValueParser; } /** @@ -34,31 +60,38 @@ interface RowOptions { * ``` */ export class Row< - T extends CellType | undefined = CellType | undefined, -> extends Array { - protected options: RowOptions = {}; + TCell extends CellType, +> extends Array { + protected options: RowOptions> = {}; /** * Create a new row. If cells is a row, all cells and options of the row will * be copied to the new row. * - * @param cells Cells or row. + * @param value Cells or row. */ - public static from( - cells: RowType, - ): Row { - const row = new this(...cells); - if (cells instanceof Row) { - row.options = { ...(cells as Row).options }; + public static from< + TCell extends CellType, + >( + value: RowType, + ): Row { + if (Array.isArray(value)) { + const row = new this(...value); + if (value instanceof Row) { + row.options = { ...(value as Row).options }; + } + return row; } - return row; + + return new this(value as TCell); } /** Clone row recursively with all options. */ - public clone(): Row { - const row = new Row( - ...this.map((cell: T) => cell instanceof Cell ? cell.clone() : cell), + public clone(): this { + const cells = this.map((cell) => + cell instanceof Cell ? cell.clone() : cell ); + const row = Row.from(cells) as this; row.options = { ...this.options }; return row; } @@ -93,13 +126,22 @@ export class Row< return this; } + /** + * Register cell value parser. + * @param fn Value parser callback function. + */ + public value(fn: ValueParser>): this { + this.options.value = fn; + return this; + } + /** * Getter: */ /** Check if row has border. */ - public getBorder(): boolean { - return this.options.border === true; + public getBorder(): boolean | undefined { + return this.options.border; } /** Check if row or any child cell has border. */ @@ -109,13 +151,18 @@ export class Row< } /** Get row alignment. */ - public getAlign(): Direction { - return this.options.align ?? "left"; + public getAlign(): Direction | undefined { + return this.options.align; + } + + /** Get value parser. */ + public getCellValueParser(): ValueParser> | undefined { + return this.options.value; } } /** @deprecated Use `RowType` instead. */ -export type IRow = RowType; +export type IRow = RowType; -/** @deprecated Use `DataRow` instead. */ -export type IDataRow = DataRow; +/** @deprecated Use `Record` instead. */ +export type IDataRow = DataRow; diff --git a/table/table.ts b/table/table.ts index ebb736ba..3aaab371 100644 --- a/table/table.ts +++ b/table/table.ts @@ -1,36 +1,20 @@ import { Border, border } from "./border.ts"; -import { Cell, Direction } from "./cell.ts"; +import { + Cell, + CellValue, + Direction, + GetCellValue, + ValueParser, +} from "./cell.ts"; import { Column, ColumnOptions } from "./column.ts"; import { TableLayout } from "./_layout.ts"; -import { DataRow, Row, RowType } from "./row.ts"; - -/** Border characters settings. */ -export type BorderOptions = Partial; - -/** Table settings. */ -export interface TableSettings { - /** Table indentation. */ - indent: number; - /** Enable/disable border on all cells. */ - border: boolean; - /** Set min column width. */ - minColWidth: number | Array; - /** Set max column width. */ - maxColWidth: number | Array; - /** Set cell padding. */ - padding: number | Array; - /** Set table characters. */ - chars: Border; - /** Set cell content alignment. */ - align?: Direction; - /** Set column options. */ - columns: Array; -} - -/** Table type. */ -export type TableType = - | Array - | Table; +import { + GetRowInnerValue, + GetRowValue, + Row, + RowType, + UnwrapRow, +} from "./row.ts"; /** * Table representation. @@ -49,18 +33,21 @@ export type TableType = * .render(); * ``` */ -export class Table extends Array { +export class Table< + TRow extends RowType, + THeaderRow extends RowType = TRow, +> extends Array { protected static _chars: Border = { ...border }; - protected options: TableSettings = { + protected options: TableSettings = { indent: 0, border: false, maxColWidth: Infinity, minColWidth: 0, padding: 1, chars: { ...Table._chars }, - columns: [], + columns: [] as Array as Columns, }; - private headerRow?: Row; + private headerRow?: Row>; /** * Create a new table. If rows is a table, all rows and options of the table @@ -68,10 +55,15 @@ export class Table extends Array { * * @param rows An array of rows or a table instance. */ - public static from(rows: TableType): Table { - const table = new this(...rows); + public static from< + TRow extends RowType, + THeaderRow extends RowType, + >( + rows: TableType, + ): Table { + const table = new this(...rows); if (rows instanceof Table) { - table.options = { ...(rows as Table).options }; + table.options = { ...(rows as Table).options }; table.headerRow = rows.headerRow ? Row.from(rows.headerRow) : undefined; } return table; @@ -83,7 +75,9 @@ export class Table extends Array { * * @param rows Array of objects. */ - public static fromJson(rows: Array): Table { + public static fromJson( + rows: Array>, + ): Table, Array> { return new this().fromJson(rows); } @@ -102,7 +96,12 @@ export class Table extends Array { * * @param rows Table or rows. */ - public static render(rows: TableType): void { + public static render< + TRow extends RowType, + THeaderRow extends RowType, + >( + rows: Array | Table, + ): void { Table.from(rows).render(); } @@ -112,21 +111,25 @@ export class Table extends Array { * * @param rows Array of objects. */ - public fromJson(rows: Array): this { - this.header(Object.keys(rows[0])); - this.body(rows.map((row) => Object.values(row) as TRow)); - return this; + public fromJson( + rows: Array>, + ): Table, Array> { + return (this as Table as Table, Array>) + .header(Object.keys(rows[0])) + .body(rows.map((row) => Object.values(row))); } /** * Set column options. * - * @param columns An array of columns or column options. + * @param columns Array of columns or column options. */ - public columns(columns: Array): this { + public columns( + columns: ColumnsOptions, + ): this { this.options.columns = columns.map((column) => column instanceof Column ? column : Column.from(column) - ); + ) as Columns; return this; } @@ -136,9 +139,11 @@ export class Table extends Array { @param index The column index. @param column Column or column options. */ - public column( - index: number, - column: Column | ColumnOptions, + public column( + index: TIndex, + column: + | Columns[TIndex] + | ColumnsOptions[TIndex], ): this { if (column instanceof Column) { this.options.columns[index] = column; @@ -147,6 +152,7 @@ export class Table extends Array { } else { this.options.columns[index] = Column.from(column); } + return this; } @@ -155,9 +161,28 @@ export class Table extends Array { * * @param header Header row or cells. */ - public header(header: RowType): this { - this.headerRow = header instanceof Row ? header : Row.from(header); - return this; + public header( + header: THeader, + ): Table { + this.headerRow = header instanceof Row + ? header + : Row.from(header) as Row>; + return this as Table as Table; + } + + /** + * Add an array of rows. + * @param rows Table rows. + */ + public rows( + rows: Array, + ): Table { + const table = this as Table, THeaderRow> as Table< + TBodyRow, + THeaderRow + >; + table.push(...rows); + return table as Table; } /** @@ -165,19 +190,19 @@ export class Table extends Array { * * @param rows Array of rows. */ - public body(rows: Array): this { + public body( + rows: Array, + ): Table { this.length = 0; - this.push(...rows); - return this; + return this.rows(rows) as Table; } /** Clone table recursively with header and options. */ - public clone(): Table { - const table = new Table( - ...this.map((row: TRow) => - row instanceof Row ? row.clone() : Row.from(row).clone() - ), + public clone(): Table { + const rows = this.map((row) => + row instanceof Row ? row.clone() : Row.from(row).clone() ); + const table = Table.from(rows) as this; table.options = { ...this.options }; table.headerRow = this.headerRow?.clone(); return table; @@ -282,8 +307,28 @@ export class Table extends Array { return this; } + /** + * Register header value parser. + * @param fn Value parser callback function. + */ + public headerValue( + fn: ValueParser>, + ): this { + this.options.headerValue = fn; + return this; + } + + /** + * Register cell value parser. + * @param fn Value parser callback function. + */ + public value(fn: ValueParser>): this { + this.options.value = fn; + return this; + } + /** Get table header. */ - public getHeader(): Row | undefined { + public getHeader(): Row> | undefined { return this.headerRow; } @@ -313,14 +358,15 @@ export class Table extends Array { } /** Check if table has border. */ - public getBorder(): boolean { - return this.options.border === true; + public getBorder(): boolean | undefined { + return this.options.border; } /** Check if header row has border. */ public hasHeaderBorder(): boolean { const hasBorder = this.headerRow?.hasBorder(); - return hasBorder === true || (this.getBorder() && hasBorder !== false); + return hasBorder === true || + (this.getBorder() === true && hasBorder !== false); } /** Check if table bordy has border. */ @@ -330,7 +376,9 @@ export class Table extends Array { this.some((row) => row instanceof Row ? row.hasBorder() - : row.some((cell) => cell instanceof Cell ? cell.getBorder() : false) + : Array.isArray(row) + ? row.some((cell) => cell instanceof Cell ? cell.getBorder() : false) + : false ); } @@ -340,23 +388,131 @@ export class Table extends Array { } /** Get table alignment. */ - public getAlign(): Direction { - return this.options.align ?? "left"; + public getAlign(): Direction | undefined { + return this.options.align; } /** Get columns. */ - public getColumns(): Array { + public getColumns(): Columns { return this.options.columns; } - /** Get column by column index. */ - public getColumn(index: number): Column { + /** Get column by index. */ + public getColumn( + index: TIndex, + ): Columns[TIndex] { return this.options.columns[index] ??= new Column(); } + + /** Get header value parser. */ + public getHeaderValueParser(): + | ValueParser> + | undefined { + return this.options.headerValue; + } + + /** Get value parser. */ + public getCellValueParser(): + | ValueParser> + | undefined { + return this.options.value; + } } +/** Table settings. */ +export interface TableSettings< + TRow extends RowType, + THeaderRow extends RowType = TRow, +> { + /** Table indentation. */ + indent: number; + /** Enable/disable border on all cells. */ + border: boolean; + /** Set min column width. */ + minColWidth: number | Array; + /** Set max column width. */ + maxColWidth: number | Array; + /** Set cell padding. */ + padding: number | Array; + /** Set table characters. */ + chars: Border; + /** Set cell content alignment. */ + align?: Direction; + /** Set column options. */ + columns: Columns; + /** Header value parser */ + headerValue?: ValueParser>; + /** Cell value parser */ + value?: ValueParser>; +} + +/** Table type. */ +export type TableType< + TRow extends RowType, + THeaderRow extends RowType = TRow, +> = + | Array + | Table; + +/** Border characters settings. */ +export type BorderOptions = Partial; + +type Columns< + TRow extends RowType, + THeaderRow extends RowType = TRow, +> = GetRow, UnwrapRow> extends infer Row + ? [] extends Row + ? Array, GetRowInnerValue>> + // deno-lint-ignore ban-types + : {} extends Row + ? Array, GetRowInnerValue>> + : { + [TIndex in keyof Row]?: Column< + GetCellValue>, + GetCellValue> + >; + } + : never; + +type ColumnsOptions< + TRow extends RowType, + THeaderRow extends RowType = TRow, +> = GetRow, UnwrapRow> extends infer Row + ? [] extends Row + ? Array, GetRowInnerValue>> + // deno-lint-ignore ban-types + : {} extends Row + ? Array, GetRowInnerValue>> + : { + [TIndex in keyof Row]?: ColumnOptions< + GetCellValue>, + GetCellValue> + >; + } + : never; + +type GetRow< + TRow, + THeaderRow, +> = TRow extends ReadonlyArray + ? THeaderRow extends ReadonlyArray ? TRow | THeaderRow + : TRow + : THeaderRow extends ReadonlyArray ? THeaderRow + : MakeArray | MakeArray; + +type GetCell = TIndex extends keyof TRow ? TRow[TIndex] + : TRow extends ReadonlyArray ? TRow[number] + : TRow; + +type MakeArray = unknown extends T ? [] + : T extends ReadonlyArray ? T + : ReadonlyArray; + /** @deprecated Use `BorderOptions` instead. */ export type IBorderOptions = BorderOptions; /** @deprecated Use `TableType` instead. */ -export type ITable = TableType; +export type ITable< + TRow extends RowType = RowType, + THeaderRow extends RowType = TRow, +> = TableType; diff --git a/table/test/__snapshots__/column_test.ts.snap b/table/test/__snapshots__/column_test.ts.snap index 291663ff..3517aab5 100644 --- a/table/test/__snapshots__/column_test.ts.snap +++ b/table/test/__snapshots__/column_test.ts.snap @@ -1,8 +1,7 @@ export const snapshot = {}; snapshot[`[table] should set border on columns 1`] = ` -stdout: -" Foo Bar Baz + Foo Bar Baz ┌─────────────┐ ┌─────────────┐ │ foo bar baz │ baz │ beep boop │ ├─────────────┤ ├─────────────┤ @@ -10,14 +9,10 @@ stdout: ├─────────────┤ ├─────────────┤ │ beep boop │ foo bar baz │ baz │ └─────────────┘ └─────────────┘ -" -stderr: -"" `; snapshot[`[table] should set align on columns 1`] = ` -stdout: -"┌─────────────┬─────────────┬─────────────┐ +┌─────────────┬─────────────┬─────────────┐ │ Foo │ Bar │ Baz │ ├─────────────┼─────────────┼─────────────┤ │ foo bar baz │ baz │ beep boop │ @@ -26,14 +21,10 @@ stdout: ├─────────────┼─────────────┼─────────────┤ │ beep boop │ foo bar baz │ baz │ └─────────────┴─────────────┴─────────────┘ -" -stderr: -"" `; snapshot[`[table] should set width on columns 1`] = ` -stdout: -"┌──────┬──────────────────────┬─────────────┐ +┌──────┬──────────────────────┬─────────────┐ │ Foo │ Bar │ Baz │ ├──────┼──────────────────────┼─────────────┤ │ foo │ baz │ beep boop │ @@ -45,30 +36,22 @@ stdout: │ beep │ foo bar baz │ baz │ │ boop │ │ │ └──────┴──────────────────────┴─────────────┘ -" -stderr: -"" `; snapshot[`[table] should set padding on columns 1`] = ` -stdout: -"┌─────────────────────┬──────────────────────────────┬─────────────────────┐ -│ Foo │ Bar │ Baz │ -├─────────────────────┼──────────────────────────────┼─────────────────────┤ -│ foo bar baz │ baz │ beep boop │ -├─────────────────────┼──────────────────────────────┼─────────────────────┤ -│ baz │ beep boop │ foo bar baz │ -├─────────────────────┼──────────────────────────────┼─────────────────────┤ -│ beep boop │ foo bar baz │ baz │ -└─────────────────────┴──────────────────────────────┴─────────────────────┘ -" -stderr: -"" +┌─────────────────────┬───────────────────┬─────────────────┐ +│ Foo │ Bar │ Baz │ +├─────────────────────┼───────────────────┼─────────────────┤ +│ foo bar baz │ baz │ beep boop │ +├─────────────────────┼───────────────────┼─────────────────┤ +│ baz │ beep boop │ foo bar baz │ +├─────────────────────┼───────────────────┼─────────────────┤ +│ beep boop │ foo bar baz │ baz │ +└─────────────────────┴───────────────────┴─────────────────┘ `; snapshot[`[table] should set column options with column method 1`] = ` -stdout: -"┌─────────────────────┬──────────────────────────────┬─────────────────────┐ +┌─────────────────────┬──────────────────────────────┬─────────────────────┐ │ Foo │ Bar │ Baz │ ├─────────────────────┼──────────────────────────────┼─────────────────────┤ │ foo bar baz │ baz │ beep boop │ @@ -77,7 +60,102 @@ stdout: ├─────────────────────┼──────────────────────────────┼─────────────────────┤ │ beep boop │ foo bar baz │ baz │ └─────────────────────┴──────────────────────────────┴─────────────────────┘ -" -stderr: -"" +`; + +snapshot[`[table] should call headerValue 1`] = ` +┌─────────────┬───────────────┬────────────────────────────────────┬──────────────────┬──────────────────────────┐ +│ Header 1: 1 │ Header 2: "2" │ Header 3: 1970-01-01T00:00:01.000Z │ Header 4: /(?:)/ │ Header 5: { foo: "bar" } │ +└─────────────┴───────────────┴────────────────────────────────────┴──────────────────┴──────────────────────────┘ +`; + +snapshot[`[table] should call headerValue and value 1`] = ` +┌─────────────┬───────────────┬────────────────────────────────────┬────────────────────────┬──────────────────────────┐ +│ Header 1: 1 │ Header 2: "2" │ Header 3: 1970-01-01T00:00:01.000Z │ Header 4: /(?:)/ │ Header 5: { foo: "bar" } │ +├─────────────┼───────────────┼────────────────────────────────────┼────────────────────────┼──────────────────────────┤ +│ Body 1: "1" │ Body 2: 2 │ Body 3: 3 │ Body 4: { beep: true } │ Body 5: [ 1 ] │ +├─────────────┼───────────────┼────────────────────────────────────┼────────────────────────┼──────────────────────────┤ +│ Body 1: "1" │ Body 2: 2 │ Body 3: "3" │ Body 4: { beep: true } │ Body 5: [ 1 ] │ +├─────────────┼───────────────┼────────────────────────────────────┼────────────────────────┼──────────────────────────┤ +│ Body 1: "1" │ Body 2: 2 │ Body 3: 3 │ Body 4: { beep: true } │ Body 5: [ 1 ] │ +└─────────────┴───────────────┴────────────────────────────────────┴────────────────────────┴──────────────────────────┘ +`; + +snapshot[`[table] should call value 1`] = ` +┌───────────────┬───────────┬─────────────┬────────────────────────┬───────────────┐ +│ Header 1: "1" │ Body 2: 2 │ Body 3: 3 │ Body 4: { beep: true } │ Body 5: [ 1 ] │ +├───────────────┼───────────┼─────────────┼────────────────────────┼───────────────┤ +│ Header 1: "1" │ Body 2: 2 │ Body 3: "3" │ Body 4: { beep: true } │ Body 5: [ 1 ] │ +├───────────────┼───────────┼─────────────┼────────────────────────┼───────────────┤ +│ Header 1: "1" │ Body 2: 2 │ Body 3: 3 │ Body 4: { beep: true } │ Body 5: [ 1 ] │ +└───────────────┴───────────┴─────────────┴────────────────────────┴───────────────┘ +`; + +snapshot[`[table] should call value and headerValue with json data 1`] = ` +┌───────────────────────────────────────┬───────────────────────────────────────┬───────────────────────────────────────┬───────────────────────────────────────┐ +│ Body 1: { │ Body 2: { │ Body 3: { │ Body 4: { │ +│ firstName: "Gino", │ firstName: "Gino", │ firstName: "Gino", │ firstName: "Gino", │ +│ lastName: "Aicheson", │ lastName: "Aicheson", │ lastName: "Aicheson", │ lastName: "Aicheson", │ +│ age: 21, │ age: 21, │ age: 21, │ age: 21, │ +│ email: "gaicheson0@nydailynews.com" │ email: "gaicheson0@nydailynews.com" │ email: "gaicheson0@nydailynews.com" │ email: "gaicheson0@nydailynews.com" │ +│ } │ } │ } │ } │ +├───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┤ +│ Body 1: { │ Body 2: { │ Body 3: { │ Body 4: { │ +│ firstName: "Godfry", │ firstName: "Godfry", │ firstName: "Godfry", │ firstName: "Godfry", │ +│ lastName: "Pedycan", │ lastName: "Pedycan", │ lastName: "Pedycan", │ lastName: "Pedycan", │ +│ age: 33, │ age: 33, │ age: 33, │ age: 33, │ +│ email: "gpedycan1@state.gov" │ email: "gpedycan1@state.gov" │ email: "gpedycan1@state.gov" │ email: "gpedycan1@state.gov" │ +│ } │ } │ } │ } │ +├───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┤ +│ Body 1: { │ Body 2: { │ Body 3: { │ Body 4: { │ +│ firstName: "Loni", │ firstName: "Loni", │ firstName: "Loni", │ firstName: "Loni", │ +│ lastName: "Miller", │ lastName: "Miller", │ lastName: "Miller", │ lastName: "Miller", │ +│ age: 24, │ age: 24, │ age: 24, │ age: 24, │ +│ email: "lmiller2@chron.com" │ email: "lmiller2@chron.com" │ email: "lmiller2@chron.com" │ email: "lmiller2@chron.com" │ +│ } │ } │ } │ } │ +└───────────────────────────────────────┴───────────────────────────────────────┴───────────────────────────────────────┴───────────────────────────────────────┘ +`; + +snapshot[`[table] should call value and headerValue with json data and header 1`] = ` +┌───────────────────────────────────────┬───────────────────────────────────────┬───────────────────────────────────────┬───────────────────────────────────────┐ +│ Header 1: 1 │ Header 2: "2" │ Header 3: 3 │ Header 4: "4" │ +├───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┤ +│ Body 1: { │ Body 2: { │ Body 3: { │ Body 4: { │ +│ firstName: "Gino", │ firstName: "Gino", │ firstName: "Gino", │ firstName: "Gino", │ +│ lastName: "Aicheson", │ lastName: "Aicheson", │ lastName: "Aicheson", │ lastName: "Aicheson", │ +│ age: 21, │ age: 21, │ age: 21, │ age: 21, │ +│ email: "gaicheson0@nydailynews.com" │ email: "gaicheson0@nydailynews.com" │ email: "gaicheson0@nydailynews.com" │ email: "gaicheson0@nydailynews.com" │ +│ } │ } │ } │ } │ +├───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┤ +│ Body 1: { │ Body 2: { │ Body 3: { │ Body 4: { │ +│ firstName: "Godfry", │ firstName: "Godfry", │ firstName: "Godfry", │ firstName: "Godfry", │ +│ lastName: "Pedycan", │ lastName: "Pedycan", │ lastName: "Pedycan", │ lastName: "Pedycan", │ +│ age: 33, │ age: 33, │ age: 33, │ age: 33, │ +│ email: "gpedycan1@state.gov" │ email: "gpedycan1@state.gov" │ email: "gpedycan1@state.gov" │ email: "gpedycan1@state.gov" │ +│ } │ } │ } │ } │ +├───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┤ +│ Body 1: { │ Body 2: { │ Body 3: { │ Body 4: { │ +│ firstName: "Loni", │ firstName: "Loni", │ firstName: "Loni", │ firstName: "Loni", │ +│ lastName: "Miller", │ lastName: "Miller", │ lastName: "Miller", │ lastName: "Miller", │ +│ age: 24, │ age: 24, │ age: 24, │ age: 24, │ +│ email: "lmiller2@chron.com" │ email: "lmiller2@chron.com" │ email: "lmiller2@chron.com" │ email: "lmiller2@chron.com" │ +│ } │ } │ } │ } │ +└───────────────────────────────────────┴───────────────────────────────────────┴───────────────────────────────────────┴───────────────────────────────────────┘ +`; + +snapshot[`[table] should call headerValue and value with Row class 1`] = ` +┌─────────────┬───────────────┬────────────────────────────────────┬──────────────────┬──────────────────────────┐ +│ Header 1: 1 │ Header 2: "2" │ Header 3: 1970-01-01T00:00:01.000Z │ Header 4: /(?:)/ │ Header 5: { foo: "bar" } │ +├─────────────┼───────────────┼────────────────────────────────────┼──────────────────┼──────────────────────────┤ +│ Body 1: 1 │ Body 2: "2" │ Body 3: 1970-01-01T00:00:01.000Z │ Body 4: /(?:)/ │ Body 5: { foo: "bar" } │ +└─────────────┴───────────────┴────────────────────────────────────┴──────────────────┴──────────────────────────┘ +`; + +snapshot[`[table] should allow header with array and body with array and row class 1`] = ` +┌───────────────────┬───────────────┬────────────────────────────────────┬──────────────────┬──────────────────────────┐ +│ Header 1: 1 │ Header 2: "2" │ Header 3: 1970-01-01T00:00:01.000Z │ Header 4: /(?:)/ │ Header 5: { foo: "bar" } │ +├───────────────────┼───────────────┼────────────────────────────────────┼──────────────────┼──────────────────────────┤ +│ Body 1: 1 │ Body 2: "2" │ Body 3: 1970-01-01T00:00:01.000Z │ Body 4: /(?:)/ │ Body 5: { foo: "bar" } │ +├───────────────────┼───────────────┼────────────────────────────────────┼──────────────────┼──────────────────────────┤ +│ Body 1: Map(0) {} │ Body 2: "2" │ Body 3: 1970-01-01T00:00:01.000Z │ Body 4: /(?:)/ │ Body 5: { foo: "bar" } │ +└───────────────────┴───────────────┴────────────────────────────────────┴──────────────────┴──────────────────────────┘ `; diff --git a/table/test/align_test.ts b/table/test/align_test.ts index e4df8472..1d0fe286 100644 --- a/table/test/align_test.ts +++ b/table/test/align_test.ts @@ -56,11 +56,11 @@ Deno.test("table - align - default direction", () => { const cell = new Cell("foo"); const row = new Row(cell); const table = new Table(row); - assertEquals(cell.getAlign(), "left"); - assertEquals(row.getAlign(), "left"); - assertEquals(table.getAlign(), "left"); - assertEquals(table[0][0].getAlign(), "left"); - assertEquals(table[0].getAlign(), "left"); + assertEquals(cell.getAlign(), undefined); + assertEquals(row.getAlign(), undefined); + assertEquals(table.getAlign(), undefined); + assertEquals(table[0][0].getAlign(), undefined); + assertEquals(table[0].getAlign(), undefined); }); Deno.test("table - align - override direction", () => { @@ -78,9 +78,9 @@ Deno.test("table - align - inherit direction", () => { const cell = new Cell("foo"); const row = new Row(cell); const table = new Table(row).align("right"); - assertEquals(cell.getAlign(), "left"); - assertEquals(row.getAlign(), "left"); + assertEquals(cell.getAlign(), undefined); + assertEquals(row.getAlign(), undefined); assertEquals(table.getAlign(), "right"); - assertEquals(table[0][0].getAlign(), "left"); - assertEquals(table[0].getAlign(), "left"); + assertEquals(table[0][0].getAlign(), undefined); + assertEquals(table[0].getAlign(), undefined); }); diff --git a/table/test/column_test.ts b/table/test/column_test.ts index 0e2a6f1d..c5175008 100644 --- a/table/test/column_test.ts +++ b/table/test/column_test.ts @@ -1,5 +1,7 @@ -import { snapshotTest } from "../../testing/snapshot.ts"; import { Table } from "../table.ts"; +import { assertSnapshot, assertType, IsExact } from "../../dev_deps.ts"; +import { Row } from "../row.ts"; +import { Cell } from "../cell.ts"; const createTable = () => new Table() @@ -10,81 +12,91 @@ const createTable = () => ["beep boop", "foo bar baz", "baz"], ]); -await snapshotTest({ +Deno.test({ name: "[table] should set border on columns", - meta: import.meta, - fn() { - createTable() - .columns([{ - border: true, - }, { - border: false, - }, { - border: true, - }]) - .render(); + async fn(t) { + await assertSnapshot( + t, + createTable() + .columns([{ + border: true, + }, { + border: false, + }, { + border: true, + }]) + .toString(), + { serializer }, + ); }, }); -await snapshotTest({ +Deno.test({ name: "[table] should set align on columns", - meta: import.meta, - fn() { - createTable() - .columns([{ - align: "left", - }, { - align: "center", - }, { - align: "right", - }]) - .border() - .render(); + async fn(t) { + await assertSnapshot( + t, + createTable() + .columns([{ + align: "left", + }, { + align: "center", + }, { + align: "right", + }]) + .border() + .toString(), + { serializer }, + ); }, }); -await snapshotTest({ +Deno.test({ name: "[table] should set width on columns", - meta: import.meta, - fn() { - createTable() - .columns([{ - maxWidth: 4, - }, { - minWidth: 20, - }, { - align: "right", - }]) - .border() - .render(); + async fn(t) { + await assertSnapshot( + t, + createTable() + .columns([{ + maxWidth: 4, + }, { + minWidth: 20, + }, { + align: "right", + }]) + .border() + .toString(), + { serializer }, + ); }, }); -await snapshotTest({ +Deno.test({ name: "[table] should set padding on columns", - meta: import.meta, - fn() { - createTable() - .columns([{ - padding: 5, - align: "left", - }, { - padding: 5, - minWidth: 20, - align: "center", - }, { - padding: 5, - align: "right", - }]) - .border() - .render(); + async fn(t) { + await assertSnapshot( + t, + createTable() + .columns([{ + padding: 5, + align: "left", + }, { + padding: 4, + align: "center", + }, { + padding: 3, + align: "right", + }]) + .border() + .toString(), + { serializer }, + ); }, }); -await snapshotTest({ +Deno.test({ name: "[table] should set column options with column method", - meta: import.meta, - fn() { + async fn(t) { const table = createTable(); table.getColumn(0)?.padding(5); table.getColumn(0)?.align("left"); @@ -96,8 +108,535 @@ await snapshotTest({ table.getColumn(2)?.padding(5); table.getColumn(2)?.align("right"); - table + await assertSnapshot( + t, + table + .border() + .toString(), + { serializer }, + ); + }, +}); + +/** Generic type tests */ + +Deno.test({ + name: "[table] should call headerValue", + fn: async (t) => { + await assertSnapshot( + t, + new Table() + .border() + .header([1, "2", new Date(1000), new RegExp(""), { + foo: "bar", + }]) + .columns([{ + headerValue: (value) => { + assertType>(true); + return `Header 1: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 2: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 3: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 4: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 5: ${Deno.inspect(value)}`; + }, + }]) + .toString(), + { serializer }, + ); + }, +}); + +Deno.test({ + name: "[table] should call headerValue and value", + fn: async (t) => { + await assertSnapshot( + t, + new Table() + .border() + .header([1, "2", new Date(1000), new RegExp(""), { foo: "bar" }]) + .body([ + ["1", 2, 3, { beep: true }, [1]], + ["1", 2, "3", { beep: true }, [1]], + ["1", 2, 3, { beep: true }, [1]], + ]) + .columns([{ + headerValue: (value) => { + assertType>(true); + return `Header 1: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 1: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 2: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 2: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 3: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 3: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 4: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 4: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 5: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 5: ${Deno.inspect(value)}`; + }, + }]) + .toString(), + { serializer }, + ); + }, +}); + +Deno.test({ + name: "[table] should call value", + fn: async (t) => { + await assertSnapshot( + t, + new Table() + .border() + .body([ + ["1", 2, 3, { beep: true }, [1]], + ["1", 2, "3", { beep: true }, [1]], + ["1", 2, 3, { beep: true }, [1]], + ]) + .columns([{ + headerValue: (value) => { + assertType>(true); + return `Header 1: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Header 1: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 2: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 2: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 3: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 3: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 4: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 4: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 5: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 5: ${Deno.inspect(value)}`; + }, + }]) + .toString(), + { serializer }, + ); + }, +}); + +Deno.test({ + name: "[table] should call value and headerValue with json data", + fn: async (t) => { + type Data = + | { + readonly firstName: "Gino"; + readonly lastName: "Aicheson"; + readonly age: 21; + readonly email: "gaicheson0@nydailynews.com"; + } + | { + readonly firstName: "Godfry"; + readonly lastName: "Pedycan"; + readonly age: 33; + readonly email: "gpedycan1@state.gov"; + } + | { + readonly firstName: "Loni"; + readonly lastName: "Miller"; + readonly age: 24; + readonly email: "lmiller2@chron.com"; + }; + + await assertSnapshot( + t, + new Table() + .border() + .body([ + { + firstName: "Gino", + lastName: "Aicheson", + age: 21, + email: "gaicheson0@nydailynews.com", + }, + { + firstName: "Godfry", + lastName: "Pedycan", + age: 33, + email: "gpedycan1@state.gov", + }, + { + firstName: "Loni", + lastName: "Miller", + age: 24, + email: "lmiller2@chron.com", + }, + ]) + .columns([{ + headerValue: (value) => { + assertType>(true); + return `Header 1: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 1: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 2: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 2: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 3: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 3: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 4: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 4: ${Deno.inspect(value)}`; + }, + }]) + .toString(), + { serializer }, + ); + }, +}); + +Deno.test({ + name: "[table] should call value and headerValue with json data and header", + fn: async (t) => { + type Data = + | { + readonly firstName: "Gino"; + readonly lastName: "Aicheson"; + readonly age: 21; + readonly email: "gaicheson0@nydailynews.com"; + } + | { + readonly firstName: "Godfry"; + readonly lastName: "Pedycan"; + readonly age: 33; + readonly email: "gpedycan1@state.gov"; + } + | { + readonly firstName: "Loni"; + readonly lastName: "Miller"; + readonly age: 24; + readonly email: "lmiller2@chron.com"; + }; + + await assertSnapshot( + t, + new Table() + .border() + .header([1, "2", 3, "4"]) + .body([ + { + firstName: "Gino", + lastName: "Aicheson", + age: 21, + email: "gaicheson0@nydailynews.com", + }, + { + firstName: "Godfry", + lastName: "Pedycan", + age: 33, + email: "gpedycan1@state.gov", + }, + { + firstName: "Loni", + lastName: "Miller", + age: 24, + email: "lmiller2@chron.com", + }, + ]) + .columns([{ + headerValue: (value) => { + assertType>(true); + return `Header 1: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 1: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 2: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 2: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 3: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 3: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 4: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 4: ${Deno.inspect(value)}`; + }, + }]) + .toString(), + { serializer }, + ); + }, +}); + +Deno.test({ + name: "[table] should call headerValue and value with Row class", + fn: async (t) => { + type ExpectedType = number | string | Date | RegExp | { foo: string }; + + await assertSnapshot( + t, + new Table() + .border() + .header( + Row.from( + [1, "2", new Date(1000), new Cell(new RegExp("")), { + foo: "bar", + }], + ), + ) + .body([ + Row.from( + [1, "2", new Date(1000), new Cell(new RegExp("")), { + foo: "bar", + }], + ), + ]) + .columns([{ + headerValue: (value) => { + assertType>(true); + return `Header 1: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 1: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 2: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 2: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 3: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 3: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 4: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 4: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 5: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 5: ${Deno.inspect(value)}`; + }, + }]) + .toString(), + { serializer }, + ); + }, +}); + +Deno.test({ + name: + "[table] should allow header with array and body with array and row class", + fn: async (t) => { + type ExpectedType = + | number + | string + | Date + | RegExp + | Map + | { foo: string }; + + const table = new Table() .border() - .render(); + .header( + [1, "2", new Date(1000), new Cell(new RegExp("")), { + foo: "bar", + }], + ) + .body([ + Row.from( + [1, "2", new Date(1000), new Cell(new RegExp("")), { + foo: "bar", + }], + ), + [ + new Map(), + "2", + new Date(1000), + new Cell(new RegExp("")), + { + foo: "bar", + }, + ], + ]) + .columns([{ + headerValue: (value) => { + assertType>(true); + return `Header 1: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 1: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 2: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 2: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 3: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 3: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 4: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 4: ${Deno.inspect(value)}`; + }, + }, { + headerValue: (value) => { + assertType>(true); + return `Header 5: ${Deno.inspect(value)}`; + }, + value: (value) => { + assertType>(true); + return `Body 5: ${Deno.inspect(value)}`; + }, + }]); + + await assertSnapshot( + t, + table.toString(), + { serializer }, + ); }, }); + +function serializer(value: T): T { + return value; +}