summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/console.md41
-rw-r--r--lib/console.js129
-rw-r--r--lib/internal/cli_table.js83
-rw-r--r--node.gyp1
-rw-r--r--test/parallel/test-console-table.js196
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;
diff --git a/node.gyp b/node.gyp
index 384439b114..cb305b93e9 100644
--- a/node.gyp
+++ b/node.gyp
@@ -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 │
+├───────────────────┼─────┼────────┤
+└───────────────────┴─────┴────────┘
+`);