console: add table method

PR-URL: https://github.com/nodejs/node/pull/18137
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
This commit is contained in:
Gus Caplan 2018-01-13 13:55:19 -06:00
parent 83d44bee01
commit 97ace04492
No known key found for this signature in database
GPG Key ID: F00BD11880E82F0E
5 changed files with 449 additions and 1 deletions

View File

@ -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 cant 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

View File

@ -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;

83
lib/internal/cli_table.js Normal file
View File

@ -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;

View File

@ -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',

View File

@ -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
`);