Skip to content

Commit

Permalink
feat(table)!: add value and headerValue options (#666)
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar authored Dec 21, 2023
1 parent aa1311f commit 393daf4
Show file tree
Hide file tree
Showing 13 changed files with 1,302 additions and 294 deletions.
2 changes: 1 addition & 1 deletion command/upgrade/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export abstract class Provider {
);

if (versions.length > this.maxListSize) {
const table = new Table().indent(indent);
const table = new Table<Array<string>>().indent(indent);
const rowSize = Math.ceil(versions.length / maxCols);
const colSize = Math.min(versions.length, maxCols);
let versionIndex = 0;
Expand Down
2 changes: 1 addition & 1 deletion examples/table/basic_usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
40 changes: 40 additions & 0 deletions examples/table/datatable.ts
Original file line number Diff line number Diff line change
@@ -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();
4 changes: 2 additions & 2 deletions examples/table/random_table_demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion examples/table/using_as_array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
132 changes: 89 additions & 43 deletions table/_layout.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -17,15 +23,18 @@ interface RenderSettings {
}

/** Table layout renderer. */
export class TableLayout {
export class TableLayout<
TRow extends RowType<CellValue>,
THeaderRow extends RowType<CellValue>,
> {
/**
* Table layout constructor.
* @param table Table instance.
* @param options Render options.
*/
public constructor(
private table: Table,
private options: TableSettings,
private table: Table<TRow, THeaderRow>,
private options: TableSettings<TRow, THeaderRow>,
) {}

/** Generate table string. */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -100,9 +112,11 @@ export class TableLayout {
}

#getRows(): Array<Row<Cell>> {
const header: Row | undefined = this.table.getHeader();
const header: Row<CellValue> | 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)
)
Expand All @@ -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<Cell>;
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<RowType>) {
protected spanRows(rows: Array<RowType<CellType>>) {
const rowSpan: Array<number> = [];
let colSpan = 1;
let rowIndex = -1;
Expand All @@ -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<unknown>(rows[rowIndex] || []);
let colIndex = -1;

while (true) {
Expand All @@ -166,10 +191,11 @@ export class TableLayout {

if (rowSpan[colIndex] > 1) {
rowSpan[colIndex]--;
rows[rowIndex].splice(
const prevRow = rows[rowIndex - 1] as Row<CellType>;
row.splice(
colIndex,
this.getDeleteCount(rows, rowIndex, colIndex),
rows[rowIndex - 1][colIndex],
prevRow[colIndex],
);

continue;
Expand All @@ -191,51 +217,70 @@ export class TableLayout {
}

protected getDeleteCount(
rows: Array<Array<unknown>>,
rows: Array<RowType<CellType>>,
rowIndex: number,
colIndex: number,
) {
return colIndex <= rows[rowIndex].length - 1 &&
typeof rows[rowIndex][colIndex] === "undefined"
const row: RowType<CellType> = 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<Cell> {
return Row.from(row)
.border(this.table.getBorder(), false)
.align(this.table.getAlign(), false) as Row<Cell>;
}

/**
* 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<CellType>,
rowIndex: number,
colIndex: number,
): Cell {
const column: Column | undefined = this.options.columns.at(colIndex);
const column:
| Column<GetRowInnerValue<TRow>, GetRowInnerValue<THeaderRow>>
| 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<CellValue>;

if (cellValueParser) {
cell.value(cellValueParser);
cell.setValue(cellValueParser(cell.getValue()));
}

return cell;
}

private isHeaderRow(rowIndex: number) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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;
Expand Down
Loading

0 comments on commit 393daf4

Please sign in to comment.