diff --git a/table/cell.ts b/table/cell.ts index fd4820eb..e1f6ce19 100644 --- a/table/cell.ts +++ b/table/cell.ts @@ -4,12 +4,20 @@ export type ICell = number | string | String | Cell; export type Direction = "left" | "right" | "center"; +export type ValueParser = ( + value: string | number | undefined | null, +) => string | number | undefined | null; + +export type Renderer = (value: string) => string; + /** Cell options. */ export interface ICellOptions { border?: boolean; colSpan?: number; rowSpan?: number; align?: Direction; + value?: ValueParser; + render?: Renderer; } /** Cell representation. */ @@ -36,21 +44,23 @@ export class Cell { /** * Cell constructor. - * @param value Cell value. + * @param cellValue Cell value. */ - public constructor(private value: ICell) {} + public constructor( + private cellValue?: ICell | undefined | null, + ) {} /** Get cell value. */ public toString(): string { - return this.value.toString(); + return this.cellValue?.toString() ?? ""; } /** * Set cell value. * @param value Cell or cell value. */ - public setValue(value: ICell): this { - this.value = value; + public setValue(value: ICell | undefined | null): this { + this.cellValue = value; return this; } @@ -116,13 +126,31 @@ 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; + } + + /** + * Register cell renderer. Will be called once for each line in the cell. + * @param fn Cell renderer callback function. + */ + public renderer(fn: Renderer): this { + this.options.render = 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. */ @@ -139,8 +167,18 @@ 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; + } + + /** Get cell renderer. */ + public getRenderer(): Renderer | undefined { + return this.options.render; } } diff --git a/table/column.ts b/table/column.ts index 8cc63b93..8573a060 100644 --- a/table/column.ts +++ b/table/column.ts @@ -1,4 +1,4 @@ -import { Direction } from "./cell.ts"; +import { Direction, Renderer, ValueParser } from "./cell.ts"; export interface ColumnOptions { border?: boolean; @@ -6,64 +6,159 @@ export interface ColumnOptions { minWidth?: number; maxWidth?: number; padding?: number; + headerValue?: ValueParser; + cellValue?: ValueParser; + headerRenderer?: Renderer; + cellRenderer?: Renderer; } export class Column { - static from(options: ColumnOptions): Column { + /** + * Create column from existing column or column options. + * @param options Column options. + */ + static from(options: ColumnOptions | Column): Column { const column = new Column(); - column.opts = { ...options }; + column.opts = { ...options instanceof Column ? options.opts : options }; return column; } - protected opts: ColumnOptions = {}; + constructor( + protected opts: ColumnOptions = {}, + ) {} + /** + * Set column options. + * @param options Column options. + */ options(options: ColumnOptions): this { Object.assign(this.opts, options); return this; } + /** + * Set min column width. + * @param width Min column width. + */ minWidth(width: number): this { this.opts.minWidth = width; return this; } + /** + * Set max column width. + * @param width Max column width. + */ maxWidth(width: number): this { this.opts.maxWidth = width; return this; } + /** + * Set column border. + * @param border + */ border(border = true): this { this.opts.border = border; return this; } + /** + * Set column left and right padding. + * @param padding Padding. + */ padding(padding: number): this { this.opts.padding = padding; return this; } + /** + * 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. + */ + cellValue(fn: ValueParser): this { + this.opts.cellValue = fn; + return this; + } + + /** + * Register header cell renderer. Will be called once for each line in the cell. + * @param fn Cell renderer callback function. + */ + headerRenderer(fn: Renderer): this { + this.opts.headerRenderer = fn; + return this; + } + + /** + * Register cell renderer. Will be called once for each line in the cell. + * @param fn Cell renderer callback function. + */ + cellRenderer(fn: Renderer): this { + this.opts.cellRenderer = fn; + return this; + } + + /** Get min column width. */ getMinWidth(): number | undefined { return this.opts.minWidth; } + /** Get max column width. */ getMaxWidth(): number | undefined { return this.opts.maxWidth; } + /** Get column border. */ getBorder(): boolean | undefined { return this.opts.border; } + /** Get column padding. */ getPadding(): number | undefined { return this.opts.padding; } + /** Get column alignment. */ 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.cellValue; + } + + /** Get header renderer. */ + getHeaderRenderer(): Renderer | undefined { + return this.opts.headerRenderer; + } + + /** Get cell renderer. */ + getCellRenderer(): Renderer | undefined { + return this.opts.cellRenderer; + } } diff --git a/table/layout.ts b/table/layout.ts index 47c6cd50..08d39c19 100644 --- a/table/layout.ts +++ b/table/layout.ts @@ -53,12 +53,13 @@ export class TableLayout { const rows = this.#getRows(); const columns: number = Math.max(...rows.map((row) => row.length)); - for (const row of rows) { + for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + const row = rows[rowIndex]; const length: number = row.length; if (length < columns) { const diff = columns - length; for (let i = 0; i < diff; i++) { - row.push(this.createCell(null, row, length + i)); + row.push(this.createCell(null, row, rowIndex, length + i)); } } } @@ -110,11 +111,18 @@ export class TableLayout { return this.spanRows(rows); } - return rows.map((row) => { - const newRow = this.createRow(row); + return rows.map((row, rowIndex) => { + const newRow = Row.from(row) as Row; + for (let colIndex = 0; colIndex < row.length; colIndex++) { - newRow[colIndex] = this.createCell(row[colIndex], newRow, colIndex); + newRow[colIndex] = this.createCell( + row[colIndex], + newRow, + rowIndex, + colIndex, + ); } + return newRow; }); } @@ -133,7 +141,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) { @@ -171,6 +179,7 @@ export class TableLayout { const cell = row[colIndex] = this.createCell( row[colIndex] || null, row, + rowIndex, colIndex, ); @@ -193,30 +202,57 @@ export class TableLayout { : 0; } - /** - * Create a new row from existing row or cell array. - * @param row Original row. - */ - protected createRow(row: IRow): 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 row Parent row. - */ + /** Create a new cell from existing cell or cell value. */ protected createCell( - cell: ICell | null | undefined, + value: ICell | null | undefined, row: Row, + rowIndex: number, colIndex: number, ): Cell { const column: Column | undefined = this.options.columns.at(colIndex); - return Cell.from(cell ?? "") - .border(column?.getBorder() ?? row.getBorder(), false) - .align(column?.getAlign() ?? row.getAlign(), false); + const isHeader = rowIndex === 0 && this.table.getHeader() !== undefined; + const cell = Cell.from(value ?? ""); + + if (typeof cell.getBorder() === "undefined") { + cell.border( + row.getBorder() ?? column?.getBorder() ?? this.table.getBorder() ?? + false, + ); + } + + if (!cell.getAlign()) { + cell.align( + row.getAlign() ?? column?.getAlign() ?? this.table.getAlign() ?? "left", + ); + } + + if (!cell.getRenderer()) { + const cellRenderer = row.getCellRenderer() ?? ( + isHeader ? column?.getHeaderRenderer() : column?.getCellRenderer() + ) ?? ( + isHeader ? this.table.getHeaderRenderer() : this.table.getCellRenderer() + ); + + if (cellRenderer) { + cell.renderer(cellRenderer); + } + } + + const cellValueParser = cell.getValueParser() ?? row.getCellValueParser() ?? + ( + isHeader ? column?.getHeaderValueParser() : column?.getCellValueParser() + ) ?? ( + isHeader + ? this.table.getHeaderValueParser() + : this.table.getCellValueParser() + ); + + if (cellValueParser) { + cell.value(cellValueParser); + cell.setValue(cellValueParser(cell.toString())); + } + + return cell; } /** @@ -373,7 +409,7 @@ export class TableLayout { result += " ".repeat(opts.padding[colIndex]); } - result += current; + result += row[colIndex].getRenderer()?.(current) ?? current; if (opts.hasBorder || colIndex < opts.columns - 1) { result += " ".repeat(opts.padding[colIndex]); @@ -409,7 +445,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/row.ts b/table/row.ts index 381ebe26..b60ae620 100644 --- a/table/row.ts +++ b/table/row.ts @@ -1,4 +1,4 @@ -import { Cell, Direction, ICell } from "./cell.ts"; +import { Cell, Direction, ICell, Renderer, ValueParser } from "./cell.ts"; /** Row type */ export type IRow = @@ -12,6 +12,10 @@ export interface IRowOptions { indent?: number; border?: boolean; align?: Direction; + cellValue?: ( + value: string | number | undefined | null, + ) => string | number | undefined | null; + cellRenderer?: (value: string) => string; } /** @@ -73,13 +77,31 @@ export class Row return this; } + /** + * Register cell value parser. + * @param fn Value parser callback function. + */ + public cellValue(fn: ValueParser): this { + this.options.cellValue = fn; + return this; + } + + /** + * Register cell renderer. Will be called once for each line in the cell. + * @param fn Cell renderer callback function. + */ + public cellRenderer(fn: Renderer): this { + this.options.cellRenderer = 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. */ @@ -89,7 +111,17 @@ 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.cellValue; + } + + /** Get cell renderer. */ + public getCellRenderer(): Renderer | undefined { + return this.options.cellRenderer; } } diff --git a/table/table.ts b/table/table.ts index c33b2e86..d248eb68 100644 --- a/table/table.ts +++ b/table/table.ts @@ -1,5 +1,5 @@ import { border, IBorder } from "./border.ts"; -import { Cell, Direction } from "./cell.ts"; +import { Cell, Direction, Renderer, ValueParser } from "./cell.ts"; import { Column, ColumnOptions } from "./column.ts"; import { TableLayout } from "./layout.ts"; import { IDataRow, IRow, Row } from "./row.ts"; @@ -16,13 +16,31 @@ export interface ITableOptions { minColWidth?: number | number[]; padding?: number | number[]; chars?: IBorderOptions; + headerValue?: ValueParser; + cellValue?: ValueParser; + headerRenderer?: Renderer; + cellRenderer?: Renderer; } /** Table settings. */ -export interface ITableSettings extends Required> { +export interface ITableSettings extends + Required< + Omit< + ITableOptions, + | "align" + | "headerValue" + | "cellValue" + | "headerRenderer" + | "cellRenderer" + > + > { chars: IBorder; align?: Direction; columns: Array; + headerValue?: ValueParser; + cellValue?: ValueParser; + headerRenderer?: Renderer; + cellRenderer?: Renderer; } /** Table type. */ @@ -93,6 +111,10 @@ export class Table extends Array { return this; } + /** + * Set column definitions. + * @param columns Array of columns or column options. + */ public columns(columns: Array): this { this.options.columns = columns.map((column) => column instanceof Column ? column : Column.from(column) @@ -100,6 +122,11 @@ export class Table extends Array { return this; } + /** + * Set column definitions for a single column. + * @param index Column index. + * @param column Column or column options. + */ public column( index: number, column: Column | ColumnOptions, @@ -237,6 +264,42 @@ 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 cellValue(fn: ValueParser): this { + this.options.cellValue = fn; + return this; + } + + /** + * Register header renderer. Will be called once for each line in the cell. + * @param fn Cell renderer callback function. + */ + public headerRenderer(fn: Renderer): this { + this.options.headerRenderer = fn; + return this; + } + + /** + * Register cell renderer. Will be called once for each line in the cell. + * @param fn Cell renderer callback function. + */ + public cellRenderer(fn: Renderer): this { + this.options.cellRenderer = fn; + return this; + } + /** Get table header. */ public getHeader(): Row | undefined { return this.headerRow; @@ -247,7 +310,7 @@ export class Table extends Array { return [...this]; } - /** Get mac col widrth. */ + /** Get max col width. */ public getMaxColWidth(): number | number[] { return this.options.maxColWidth; } @@ -268,14 +331,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. */ @@ -295,15 +359,37 @@ 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 column definitions. */ public getColumns(): Array { return this.options.columns; } + /** Get column definition by column index. */ public getColumn(index: number): Column { 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.cellValue; + } + + /** Get header renderer. */ + public getHeaderRenderer(): Renderer | undefined { + return this.options.headerRenderer; + } + + /** Get cell renderer. */ + public getCellRenderer(): Renderer | undefined { + return this.options.cellRenderer; + } } diff --git a/table/test/__snapshots__/column_test.ts.snap b/table/test/__snapshots__/column_test.ts.snap new file mode 100644 index 00000000..0c53444f --- /dev/null +++ b/table/test/__snapshots__/column_test.ts.snap @@ -0,0 +1,13 @@ +export const snapshot = {}; + +snapshot[`[table] should call parser and renderer callback methods 1`] = ` +"┌──────────────┬──────────────┬──────────────┐ +│ \\x1b[35mFooa \\x1b[39m │ \\x1b[34mBarb \\x1b[39m │ \\x1b[33mBazc \\x1b[39m │ +├──────────────┼──────────────┼──────────────┤ +│ \\x1b[34mfoo bar baz1\\x1b[39m │ \\x1b[35mbaz2 \\x1b[39m │ \\x1b[32mbeep boop3 \\x1b[39m │ +├──────────────┼──────────────┼──────────────┤ +│ \\x1b[34mbaz1 \\x1b[39m │ \\x1b[35mbeep boop2 \\x1b[39m │ \\x1b[32mfoo bar baz3\\x1b[39m │ +├──────────────┼──────────────┼──────────────┤ +│ \\x1b[34mbeep boop1 \\x1b[39m │ \\x1b[35mfoo bar baz2\\x1b[39m │ \\x1b[32mbaz3 \\x1b[39m │ +└──────────────┴──────────────┴──────────────┘" +`; diff --git a/table/test/align_test.ts b/table/test/align_test.ts index b0f2fce7..1576b048 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 50342149..6326ad89 100644 --- a/table/test/column_test.ts +++ b/table/test/column_test.ts @@ -1,5 +1,6 @@ +import { colors } from "../../ansi/colors.ts"; import { Table } from "../table.ts"; -import { assertEquals } from "../../dev_deps.ts"; +import { assertEquals, assertSnapshot } from "../../dev_deps.ts"; const createTable = () => new Table() @@ -117,6 +118,31 @@ Deno.test("[table] should set padding on columns", () => { ); }); +Deno.test("[table] should call parser and renderer callback methods", async (t) => { + await assertSnapshot( + t, + createTable() + .columns([{ + headerValue: (value) => value + "a", + cellValue: (value) => value + "1", + headerRenderer: colors.magenta, + cellRenderer: colors.blue, + }, { + headerValue: (value) => value + "b", + cellValue: (value) => value + "2", + headerRenderer: colors.blue, + cellRenderer: colors.magenta, + }, { + headerValue: (value) => value + "c", + cellValue: (value) => value + "3", + headerRenderer: colors.yellow, + cellRenderer: colors.green, + }]) + .border(true) + .toString(), + ); +}); + Deno.test("[table] should set column options with column method", () => { const table = createTable(); table.getColumn(0)?.padding(5);