diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/console.ts | 94 | ||||
-rw-r--r-- | js/console_table.ts | 95 | ||||
-rw-r--r-- | js/console_test.ts | 4 |
3 files changed, 192 insertions, 1 deletions
diff --git a/js/console.ts b/js/console.ts index 60d4101f1..0797ab8e9 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<any>; @@ -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<any>(), + 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 000000000..f04e5915a --- /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 e34e25d8f..ee13c8229 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"); |