diff options
-rw-r--r-- | doc/api/console.md | 41 | ||||
-rw-r--r-- | lib/console.js | 129 | ||||
-rw-r--r-- | lib/internal/cli_table.js | 83 | ||||
-rw-r--r-- | node.gyp | 1 | ||||
-rw-r--r-- | test/parallel/test-console-table.js | 196 |
5 files changed, 449 insertions, 1 deletions
diff --git a/doc/api/console.md b/doc/api/console.md index 55adfc1e83..72d80ab9e7 100644 --- a/doc/api/console.md +++ b/doc/api/console.md @@ -332,6 +332,47 @@ console.log('count:', count); See [`util.format()`][] for more information. +### console.table(tabularData[, properties]) +<!-- YAML +added: REPLACEME +--> + +* `tabularData` {any} +* `properties` {string[]} Alternate properties for constructing the table. + +Try to construct a table with the columns of the properties of `tabularData` +(or use `properties`) and rows of `tabularData` and logit. Falls back to just +logging the argument if it can’t be parsed as tabular. + +```js +// These can't be parsed as tabular data +console.table(Symbol()); +// Symbol() + +console.table(undefined); +// undefined +``` + +```js +console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }]); +// ┌─────────┬─────┬─────┐ +// │ (index) │ a │ b │ +// ├─────────┼─────┼─────┤ +// │ 0 │ 1 │ 'Y' │ +// │ 1 │ 'Z' │ 2 │ +// └─────────┴─────┴─────┘ +``` + +```js +console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }], ['a']); +// ┌─────────┬─────┐ +// │ (index) │ a │ +// ├─────────┼─────┤ +// │ 0 │ 1 │ +// │ 1 │ 'Z' │ +// └─────────┴─────┘ +``` + ### console.time(label) <!-- YAML added: v0.1.104 diff --git a/lib/console.js b/lib/console.js index d70a6b30b7..456c0cc439 100644 --- a/lib/console.js +++ b/lib/console.js @@ -23,11 +23,31 @@ const { isStackOverflowError, - codes: { ERR_CONSOLE_WRITABLE_STREAM }, + codes: { + ERR_CONSOLE_WRITABLE_STREAM, + ERR_INVALID_ARG_TYPE, + }, } = require('internal/errors'); +const { previewMapIterator, previewSetIterator } = require('internal/v8'); +const { Buffer: { isBuffer } } = require('buffer'); +const cliTable = require('internal/cli_table'); const util = require('util'); +const { + isTypedArray, isSet, isMap, isSetIterator, isMapIterator, +} = util.types; const kCounts = Symbol('counts'); +const { + keys: ObjectKeys, + values: ObjectValues, +} = Object; +const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); + +const { + isArray: ArrayIsArray, + from: ArrayFrom, +} = Array; + // Track amount of indentation required via `console.group()`. const kGroupIndent = Symbol('groupIndent'); @@ -242,6 +262,113 @@ Console.prototype.groupEnd = function groupEnd() { this[kGroupIndent].slice(0, this[kGroupIndent].length - 2); }; +const keyKey = 'Key'; +const valuesKey = 'Values'; +const indexKey = '(index)'; +const iterKey = '(iteration index)'; + + +const isArray = (v) => ArrayIsArray(v) || isTypedArray(v) || isBuffer(v); +const inspect = (v) => { + const opt = { depth: 0, maxArrayLength: 3 }; + if (v !== null && typeof v === 'object' && + !isArray(v) && ObjectKeys(v).length > 2) + opt.depth = -1; + return util.inspect(v, opt); +}; + +const getIndexArray = (length) => ArrayFrom({ length }, (_, i) => inspect(i)); + +// https://console.spec.whatwg.org/#table +Console.prototype.table = function(tabularData, properties) { + if (properties !== undefined && !ArrayIsArray(properties)) + throw new ERR_INVALID_ARG_TYPE('properties', 'Array', properties); + + if (tabularData == null || + (typeof tabularData !== 'object' && typeof tabularData !== 'function')) + return this.log(tabularData); + + const final = (k, v) => this.log(cliTable(k, v)); + + const mapIter = isMapIterator(tabularData); + if (mapIter) + tabularData = previewMapIterator(tabularData); + + if (mapIter || isMap(tabularData)) { + const keys = []; + const values = []; + let length = 0; + for (const [k, v] of tabularData) { + keys.push(inspect(k)); + values.push(inspect(v)); + length++; + } + return final([ + iterKey, keyKey, valuesKey + ], [ + getIndexArray(length), + keys, + values, + ]); + } + + const setIter = isSetIterator(tabularData); + if (setIter) + tabularData = previewSetIterator(tabularData); + + const setlike = setIter || isSet(tabularData); + if (setlike || + (properties === undefined && + (isArray(tabularData) || isTypedArray(tabularData)))) { + const values = []; + let length = 0; + for (const v of tabularData) { + values.push(inspect(v)); + length++; + } + return final([setlike ? iterKey : indexKey, valuesKey], [ + getIndexArray(length), + values, + ]); + } + + const map = {}; + let hasPrimitives = false; + const valuesKeyArray = []; + const indexKeyArray = ObjectKeys(tabularData); + + for (var i = 0; i < indexKeyArray.length; i++) { + const item = tabularData[indexKeyArray[i]]; + const primitive = item === null || + (typeof item !== 'function' && typeof item !== 'object'); + if (properties === undefined && primitive) { + hasPrimitives = true; + valuesKeyArray[i] = inspect(item); + } else { + const keys = properties || ObjectKeys(item); + for (const key of keys) { + if (map[key] === undefined) + map[key] = []; + if ((primitive && properties) || !hasOwnProperty(item, key)) + map[key][i] = ''; + else + map[key][i] = item == null ? item : inspect(item[key]); + } + } + } + + const keys = ObjectKeys(map); + const values = ObjectValues(map); + if (hasPrimitives) { + keys.push(valuesKey); + values.push(valuesKeyArray); + } + keys.unshift(indexKey); + values.unshift(indexKeyArray); + + return final(keys, values); +}; + module.exports = new Console(process.stdout, process.stderr); module.exports.Console = Console; diff --git a/lib/internal/cli_table.js b/lib/internal/cli_table.js new file mode 100644 index 0000000000..4c07d92eeb --- /dev/null +++ b/lib/internal/cli_table.js @@ -0,0 +1,83 @@ +'use strict'; + +const { Buffer } = require('buffer'); +const { removeColors } = require('internal/util'); +const HasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); + +const tableChars = { + /* eslint-disable node-core/non-ascii-character */ + middleMiddle: '─', + rowMiddle: '┼', + topRight: '┐', + topLeft: '┌', + leftMiddle: '├', + topMiddle: '┬', + bottomRight: '┘', + bottomLeft: '└', + bottomMiddle: '┴', + rightMiddle: '┤', + left: '│ ', + right: ' │', + middle: ' │ ', + /* eslint-enable node-core/non-ascii-character */ +}; + +const countSymbols = (string) => { + const normalized = removeColors(string).normalize('NFC'); + return Buffer.from(normalized, 'UCS-2').byteLength / 2; +}; + +const renderRow = (row, columnWidths) => { + let out = tableChars.left; + for (var i = 0; i < row.length; i++) { + const cell = row[i]; + const len = countSymbols(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; +}; + +const table = (head, columns) => { + const rows = []; + const columnWidths = head.map((h) => countSymbols(h)); + const longestColumn = columns.reduce((n, a) => Math.max(n, a.length), 0); + + for (var i = 0; i < head.length; i++) { + const column = columns[i]; + for (var j = 0; j < longestColumn; j++) { + if (!rows[j]) + rows[j] = []; + const v = rows[j][i] = HasOwnProperty(column, j) ? column[j] : ''; + const width = columnWidths[i] || 0; + const counted = countSymbols(v); + columnWidths[i] = Math.max(width, counted); + } + } + + const divider = columnWidths.map((i) => + tableChars.middleMiddle.repeat(i + 2)); + + const tl = tableChars.topLeft; + const tr = tableChars.topRight; + const lm = tableChars.leftMiddle; + let result = `${tl}${divider.join(tableChars.topMiddle)}${tr} +${renderRow(head, columnWidths)} +${lm}${divider.join(tableChars.rowMiddle)}${tableChars.rightMiddle} +`; + + for (const row of rows) + result += `${renderRow(row, columnWidths)}\n`; + + result += `${tableChars.bottomLeft}${ + divider.join(tableChars.bottomMiddle)}${tableChars.bottomRight}`; + + return result; +}; + +module.exports = table; @@ -81,6 +81,7 @@ 'lib/zlib.js', 'lib/internal/async_hooks.js', 'lib/internal/buffer.js', + 'lib/internal/cli_table.js', 'lib/internal/child_process.js', 'lib/internal/cluster/child.js', 'lib/internal/cluster/master.js', diff --git a/test/parallel/test-console-table.js b/test/parallel/test-console-table.js new file mode 100644 index 0000000000..39e9099dd7 --- /dev/null +++ b/test/parallel/test-console-table.js @@ -0,0 +1,196 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const { Console } = require('console'); + +const queue = []; + +const console = new Console({ write: (x) => { + queue.push(x); +}, removeListener: () => {} }, process.stderr, false); + +function test(data, only, expected) { + if (arguments.length === 2) { + expected = only; + only = undefined; + } + console.table(data, only); + assert.strictEqual(queue.shift(), expected.trimLeft()); +} + +common.expectsError(() => console.table([], false), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +test(null, 'null\n'); +test(undefined, 'undefined\n'); +test(false, 'false\n'); +test('hi', 'hi\n'); +test(Symbol(), 'Symbol()\n'); + +test([1, 2, 3], ` +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└─────────┴────────┘ +`); + +test([Symbol(), 5, [10]], ` +┌─────────┬──────────┐ +│ (index) │ Values │ +├─────────┼──────────┤ +│ 0 │ Symbol() │ +│ 1 │ 5 │ +│ 2 │ [ 10 ] │ +└─────────┴──────────┘ +`); + +test([undefined, 5], ` +┌─────────┬───────────┐ +│ (index) │ Values │ +├─────────┼───────────┤ +│ 0 │ undefined │ +│ 1 │ 5 │ +└─────────┴───────────┘ +`); + +test({ a: 1, b: Symbol(), c: [10] }, ` +┌─────────┬────┬──────────┐ +│ (index) │ 0 │ Values │ +├─────────┼────┼──────────┤ +│ a │ │ 1 │ +│ b │ │ Symbol() │ +│ c │ 10 │ │ +└─────────┴────┴──────────┘ +`); + +test(new Map([ ['a', 1], [Symbol(), [2]] ]), ` +┌───────────────────┬──────────┬────────┐ +│ (iteration index) │ Key │ Values │ +├───────────────────┼──────────┼────────┤ +│ 0 │ 'a' │ 1 │ +│ 1 │ Symbol() │ [ 2 ] │ +└───────────────────┴──────────┴────────┘ +`); + +test(new Set([1, 2, Symbol()]), ` +┌───────────────────┬──────────┐ +│ (iteration index) │ Values │ +├───────────────────┼──────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ Symbol() │ +└───────────────────┴──────────┘ +`); + +test({ a: 1, b: 2 }, ['a'], ` +┌─────────┬───┐ +│ (index) │ a │ +├─────────┼───┤ +│ a │ │ +│ b │ │ +└─────────┴───┘ +`); + +test([{ a: 1, b: 2 }, { a: 3, c: 4 }], ['a'], ` +┌─────────┬───┐ +│ (index) │ a │ +├─────────┼───┤ +│ 0 │ 1 │ +│ 1 │ 3 │ +└─────────┴───┘ +`); + +test(new Map([[1, 1], [2, 2], [3, 3]]).entries(), ` +┌───────────────────┬─────┬────────┐ +│ (iteration index) │ Key │ Values │ +├───────────────────┼─────┼────────┤ +│ 0 │ 1 │ 1 │ +│ 1 │ 2 │ 2 │ +│ 2 │ 3 │ 3 │ +└───────────────────┴─────┴────────┘ +`); + +test(new Set([1, 2, 3]).values(), ` +┌───────────────────┬────────┐ +│ (iteration index) │ Values │ +├───────────────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───────────────────┴────────┘ +`); + + +test({ a: { a: 1, b: 2, c: 3 } }, ` +┌─────────┬───┬───┬───┐ +│ (index) │ a │ b │ c │ +├─────────┼───┼───┼───┤ +│ a │ 1 │ 2 │ 3 │ +└─────────┴───┴───┴───┘ +`); + +test({ a: [1, 2] }, ` +┌─────────┬───┬───┐ +│ (index) │ 0 │ 1 │ +├─────────┼───┼───┤ +│ a │ 1 │ 2 │ +└─────────┴───┴───┘ +`); + +test({ a: [1, 2, 3, 4, 5], b: 5, c: { e: 5 } }, ` +┌─────────┬───┬───┬───┬───┬───┬───┬────────┐ +│ (index) │ 0 │ 1 │ 2 │ 3 │ 4 │ e │ Values │ +├─────────┼───┼───┼───┼───┼───┼───┼────────┤ +│ a │ 1 │ 2 │ 3 │ 4 │ 5 │ │ │ +│ b │ │ │ │ │ │ │ 5 │ +│ c │ │ │ │ │ │ 5 │ │ +└─────────┴───┴───┴───┴───┴───┴───┴────────┘ +`); + +test(new Uint8Array([1, 2, 3]), ` +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└─────────┴────────┘ +`); + +test(Buffer.from([1, 2, 3]), ` +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└─────────┴────────┘ +`); + +test({ a: undefined }, ['x'], ` +┌─────────┬───┐ +│ (index) │ x │ +├─────────┼───┤ +│ a │ │ +└─────────┴───┘ +`); + +test([], ` +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +└─────────┴────────┘ +`); + +test(new Map(), ` +┌───────────────────┬─────┬────────┐ +│ (iteration index) │ Key │ Values │ +├───────────────────┼─────┼────────┤ +└───────────────────┴─────┴────────┘ +`); |