diff --git a/BUILD.gn b/BUILD.gn index 405bc5ca37..72517055c5 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -53,6 +53,7 @@ ts_sources = [ "js/blob.ts", "js/buffer.ts", "js/chmod.ts", + "js/console_table.ts", "js/compiler.ts", "js/console.ts", "js/copy_file.ts", diff --git a/js/console.ts b/js/console.ts index 60d4101f18..0797ab8e92 100644 --- a/js/console.ts +++ b/js/console.ts @@ -2,6 +2,7 @@ import { isTypedArray } from "./util"; import { TextEncoder } from "./text_encoding"; import { File, stdout } from "./files"; +import { cliTable } from "./console_table"; // tslint:disable-next-line:no-any type ConsoleContext = Set; @@ -591,6 +592,99 @@ export class Console { } }; + // tslint:disable-next-line:no-any + table = (data: any, properties?: string[]): void => { + // tslint:disable-next-line:no-any + type Value = any; + + if (properties !== undefined && !Array.isArray(properties)) { + throw new Error( + "The 'properties' argument must be of type Array\ + . Received type string" + ); + } + + if (data === null || typeof data !== "object") { + return this.log(data); + } + + const objectValues: { [key: string]: Value[] } = {}; + const indexKeys: string[] = []; + const values: Value[] = []; + + const stringifyValue = (value: Value) => + stringifyWithQuotes( + value, + // tslint:disable-next-line:no-any + new Set(), + 0, + 1 + ); + const toTable = (header: string[], body: string[][]) => + this.log(cliTable(header, body)); + const createColumn = (value: Value, shift?: number): string[] => [ + ...(shift ? [...new Array(shift)].map(() => "") : []), + stringifyValue(value) + ]; + + let resultData = data; + const isSet = data instanceof Set; + const isMap = data instanceof Map; + const valuesKey = "Values"; + const indexKey = isSet || isMap ? "(iteration index)" : "(index)"; + + if (isSet) { + resultData = [...data]; + } else if (isMap) { + let idx = 0; + resultData = {}; + + data.forEach((k: Value, v: Value) => { + resultData[idx] = { Key: k, Values: v }; + idx++; + }); + } + + Object.keys(resultData).forEach((k, idx) => { + const value = resultData[k]; + + if (value !== null && typeof value === "object") { + Object.keys(value).forEach(k => { + const v = value[k]; + + if (properties && !properties.includes(k)) { + return; + } + + if (objectValues[k]) { + objectValues[k].push(stringifyValue(v)); + } else { + objectValues[k] = createColumn(v, idx); + } + }); + + values.push(""); + } else { + values.push(stringifyValue(value)); + } + + indexKeys.push(k); + }); + + const headerKeys = Object.keys(objectValues); + const bodyValues = Object.values(objectValues); + const header = [ + indexKey, + ...(properties || [ + ...headerKeys, + !isMap && values.length > 0 && valuesKey + ]) + ].filter(Boolean) as string[]; + const body = [indexKeys, ...bodyValues, values]; + + toTable(header, body); + }; + time = (label = "default"): void => { label = String(label); diff --git a/js/console_table.ts b/js/console_table.ts new file mode 100644 index 0000000000..f04e5915ad --- /dev/null +++ b/js/console_table.ts @@ -0,0 +1,95 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { TextEncoder } from "./text_encoding"; + +const encoder = new TextEncoder(); + +const tableChars = { + middleMiddle: "─", + rowMiddle: "┼", + topRight: "┐", + topLeft: "┌", + leftMiddle: "├", + topMiddle: "┬", + bottomRight: "┘", + bottomLeft: "└", + bottomMiddle: "┴", + rightMiddle: "┤", + left: "│ ", + right: " │", + middle: " │ " +}; + +const colorRegExp = /\u001b\[\d\d?m/g; + +function removeColors(str: string): string { + return str.replace(colorRegExp, ""); +} + +function countBytes(str: string): number { + const normalized = removeColors(String(str)).normalize("NFC"); + + return encoder.encode(normalized).byteLength; +} + +function renderRow(row: string[], columnWidths: number[]): string { + let out = tableChars.left; + for (let i = 0; i < row.length; i++) { + const cell = row[i]; + const len = countBytes(cell); + const needed = (columnWidths[i] - len) / 2; + // round(needed) + ceil(needed) will always add up to the amount + // of spaces we need while also left justifying the output. + out += `${" ".repeat(needed)}${cell}${" ".repeat(Math.ceil(needed))}`; + if (i !== row.length - 1) { + out += tableChars.middle; + } + } + out += tableChars.right; + return out; +} + +export function cliTable(head: string[], columns: string[][]): string { + const rows = []; + const columnWidths = head.map((h: string) => countBytes(h)); + const longestColumn = columns.reduce( + (n: number, a: string[]) => Math.max(n, a.length), + 0 + ); + + for (let i = 0; i < head.length; i++) { + const column = columns[i]; + for (let j = 0; j < longestColumn; j++) { + if (rows[j] === undefined) { + rows[j] = []; + } + // tslint:disable-next-line:no-any + const value = ((rows[j][i] as any) = column.hasOwnProperty(j) + ? column[j] + : ""); + const width = columnWidths[i] || 0; + const counted = countBytes(value); + columnWidths[i] = Math.max(width, counted); + } + } + + const divider = columnWidths.map((i: number) => + tableChars.middleMiddle.repeat(i + 2) + ); + + let result = + `${tableChars.topLeft}${divider.join(tableChars.topMiddle)}` + + `${tableChars.topRight}\n${renderRow(head, columnWidths)}\n` + + `${tableChars.leftMiddle}${divider.join(tableChars.rowMiddle)}` + + `${tableChars.rightMiddle}\n`; + + for (const row of rows) { + result += `${renderRow(row, columnWidths)}\n`; + } + + result += + `${tableChars.bottomLeft}${divider.join(tableChars.bottomMiddle)}` + + tableChars.bottomRight; + + return result; +} diff --git a/js/console_test.ts b/js/console_test.ts index e34e25d8f1..ee13c82291 100644 --- a/js/console_test.ts +++ b/js/console_test.ts @@ -116,7 +116,7 @@ test(function consoleTestStringifyCircular() { assertEqual( stringify(console), // tslint:disable-next-line:max-line-length - "Console { printFunc: [Function], log: [Function], debug: [Function], info: [Function], dir: [Function], warn: [Function], error: [Function], assert: [Function], count: [Function], countReset: [Function], time: [Function], timeLog: [Function], timeEnd: [Function], group: [Function], groupCollapsed: [Function], groupEnd: [Function], clear: [Function], indentLevel: 0, collapsedAt: null }" + "Console { printFunc: [Function], log: [Function], debug: [Function], info: [Function], dir: [Function], warn: [Function], error: [Function], assert: [Function], count: [Function], countReset: [Function], table: [Function], time: [Function], timeLog: [Function], timeEnd: [Function], group: [Function], groupCollapsed: [Function], groupEnd: [Function], clear: [Function], indentLevel: 0, collapsedAt: null }" ); // test inspect is working the same assertEqual(inspect(nestedObj), nestedObjExpected); @@ -278,6 +278,7 @@ test(function consoleDetachedLog() { const consoleAssert = console.assert; const consoleCount = console.count; const consoleCountReset = console.countReset; + const consoleTable = console.table; const consoleTime = console.time; const consoleTimeLog = console.timeLog; const consoleTimeEnd = console.timeEnd; @@ -293,6 +294,7 @@ test(function consoleDetachedLog() { consoleAssert(true); consoleCount("Hello world"); consoleCountReset("Hello world"); + consoleTable({ test: "Hello world" }); consoleTime("Hello world"); consoleTimeLog("Hello world"); consoleTimeEnd("Hello world"); diff --git a/tests/console_table.test b/tests/console_table.test new file mode 100644 index 0000000000..4f2c5c4ac9 --- /dev/null +++ b/tests/console_table.test @@ -0,0 +1,3 @@ +args: tests/console_table.ts --reload +check_stderr: true +output: tests/console_table.ts.out diff --git a/tests/console_table.ts b/tests/console_table.ts new file mode 100644 index 0000000000..6ff391d99a --- /dev/null +++ b/tests/console_table.ts @@ -0,0 +1,18 @@ +console.table({ a: "test", b: 1 }); +console.table({ a: { b: 10 }, b: { b: 20, c: 30 } }, ["c"]); +console.table([1, 2, [3, [4]], [5, 6], [[7], [8]]]); +console.table(new Set([1, 2, 3, "test"])); +console.table(new Map([[1, "one"], [2, "two"]])); +console.table({ + a: true, + b: { c: { d: 10 }, e: [1, 2, [5, 6]] }, + f: "test", + g: new Set([1, 2, 3, "test"]), + h: new Map([[1, "one"]]) +}); +console.table([1, "test", false, { a: 10 }, ["test", { b: 20, c: "test" }]]); +console.table([]); +console.table({}); +console.table(new Set()); +console.table(new Map()); +console.table("test"); diff --git a/tests/console_table.ts.out b/tests/console_table.ts.out new file mode 100644 index 0000000000..b4651bc659 --- /dev/null +++ b/tests/console_table.ts.out @@ -0,0 +1,71 @@ +Compiling [WILDCARD].ts +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +│ a │ "test" │ +│ b │ 1 │ +└─────────┴────────┘ +┌─────────┬────┐ +│ (index) │ c │ +├─────────┼────┤ +│ a │ │ +│ b │ 30 │ +└─────────┴────┘ +┌─────────┬───────┬───────┬────────┐ +│ (index) │ 0 │ 1 │ Values │ +├─────────┼───────┼───────┼────────┤ +│ 0 │ │ │ 1 │ +│ 1 │ │ │ 2 │ +│ 2 │ 3 │ [ 4 ] │ │ +│ 3 │ 5 │ 6 │ │ +│ 4 │ [ 7 ] │ [ 8 ] │ │ +└─────────┴───────┴───────┴────────┘ +┌───────────────────┬────────┐ +│ (iteration index) │ Values │ +├───────────────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +│ 3 │ "test" │ +└───────────────────┴────────┘ +┌───────────────────┬───────┬────────┐ +│ (iteration index) │ Key │ Values │ +├───────────────────┼───────┼────────┤ +│ 0 │ "one" │ 1 │ +│ 1 │ "two" │ 2 │ +└───────────────────┴───────┴────────┘ +┌─────────┬───────────┬───────────────────┬────────┐ +│ (index) │ c │ e │ Values │ +├─────────┼───────────┼───────────────────┼────────┤ +│ a │ │ │ true │ +│ b │ { d: 10 } │ [ 1, 2, [Array] ] │ │ +│ f │ │ │ "test" │ +│ g │ │ │ │ +│ h │ │ │ │ +└─────────┴───────────┴───────────────────┴────────┘ +┌─────────┬────────┬──────────────────────┬────┬────────┐ +│ (index) │ 0 │ 1 │ a │ Values │ +├─────────┼────────┼──────────────────────┼────┼────────┤ +│ 0 │ │ │ │ 1 │ +│ 1 │ │ │ │ "test" │ +│ 2 │ │ │ │ false │ +│ 3 │ │ │ 10 │ │ +│ 4 │ "test" │ { b: 20, c: "test" } │ │ │ +└─────────┴────────┴──────────────────────┴────┴────────┘ +┌─────────┐ +│ (index) │ +├─────────┤ +└─────────┘ +┌─────────┐ +│ (index) │ +├─────────┤ +└─────────┘ +┌───────────────────┐ +│ (iteration index) │ +├───────────────────┤ +└───────────────────┘ +┌───────────────────┐ +│ (iteration index) │ +├───────────────────┤ +└───────────────────┘ +test