Skip to content

Commit 8a0babe

Browse files
committed
console: add table method
1 parent 83d44be commit 8a0babe

File tree

5 files changed

+449
-1
lines changed

5 files changed

+449
-1
lines changed

doc/api/console.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,47 @@ console.log('count:', count);
332332

333333
See [`util.format()`][] for more information.
334334

335+
### console.table(tabularData[, properties])
336+
<!-- YAML
337+
added: REPLACEME
338+
-->
339+
340+
* `tabularData` {any}
341+
* `properties` {string[]} Alternate properties for constructing the table.
342+
343+
Try to construct a table with the columns of the properties of `tabularData`
344+
(or use `properties`) and rows of `tabularData` and logit. Falls back to just
345+
logging the argument if it can’t be parsed as tabular.
346+
347+
```js
348+
// These can't be parsed as tabular data
349+
console.table(Symbol());
350+
// Symbol()
351+
352+
console.table(undefined);
353+
// undefined
354+
```
355+
356+
```js
357+
console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }]);
358+
// ┌─────────┬─────┬─────┐
359+
// │ (index) │ a │ b │
360+
// ├─────────┼─────┼─────┤
361+
// │ 0 │ 1 │ 'Y' │
362+
// │ 1 │ 'Z' │ 2 │
363+
// └─────────┴─────┴─────┘
364+
```
365+
366+
```js
367+
console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }], ['a']);
368+
// ┌─────────┬─────┐
369+
// │ (index) │ a │
370+
// ├─────────┼─────┤
371+
// │ 0 │ 1 │
372+
// │ 1 │ 'Z' │
373+
// └─────────┴─────┘
374+
```
375+
335376
### console.time(label)
336377
<!-- YAML
337378
added: v0.1.104

lib/console.js

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,31 @@
2323

2424
const {
2525
isStackOverflowError,
26-
codes: { ERR_CONSOLE_WRITABLE_STREAM },
26+
codes: {
27+
ERR_CONSOLE_WRITABLE_STREAM,
28+
ERR_INVALID_ARG_TYPE,
29+
},
2730
} = require('internal/errors');
31+
const { previewMapIterator, previewSetIterator } = require('internal/v8');
32+
const { Buffer: { isBuffer } } = require('buffer');
33+
const cliTable = require('internal/cli_table');
2834
const util = require('util');
35+
const {
36+
isTypedArray, isSet, isMap, isSetIterator, isMapIterator,
37+
} = util.types;
2938
const kCounts = Symbol('counts');
3039

40+
const {
41+
keys: ObjectKeys,
42+
values: ObjectValues,
43+
} = Object;
44+
const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
45+
46+
const {
47+
isArray: ArrayIsArray,
48+
from: ArrayFrom,
49+
} = Array;
50+
3151
// Track amount of indentation required via `console.group()`.
3252
const kGroupIndent = Symbol('groupIndent');
3353

@@ -242,6 +262,113 @@ Console.prototype.groupEnd = function groupEnd() {
242262
this[kGroupIndent].slice(0, this[kGroupIndent].length - 2);
243263
};
244264

265+
const keyKey = 'Key';
266+
const valuesKey = 'Values';
267+
const indexKey = '(index)';
268+
const iterKey = '(iteration index)';
269+
270+
271+
const isArray = (v) => ArrayIsArray(v) || isTypedArray(v) || isBuffer(v);
272+
const inspect = (v) => {
273+
const opt = { depth: 0, maxArrayLength: 3 };
274+
if (v !== null && typeof v === 'object' &&
275+
!isArray(v) && ObjectKeys(v).length > 2)
276+
opt.depth = -1;
277+
return util.inspect(v, opt);
278+
};
279+
280+
const getIndexArray = (length) => ArrayFrom({ length }, (_, i) => inspect(i));
281+
282+
// https://console.spec.whatwg.org/#table
283+
Console.prototype.table = function(tabularData, properties) {
284+
if (properties !== undefined && !ArrayIsArray(properties))
285+
throw new ERR_INVALID_ARG_TYPE('properties', 'Array', properties);
286+
287+
if (tabularData == null ||
288+
(typeof tabularData !== 'object' && typeof tabularData !== 'function'))
289+
return this.log(tabularData);
290+
291+
const final = (k, v) => this.log(cliTable(k, v));
292+
293+
const mapIter = isMapIterator(tabularData);
294+
if (mapIter)
295+
tabularData = previewMapIterator(tabularData);
296+
297+
if (mapIter || isMap(tabularData)) {
298+
const keys = [];
299+
const values = [];
300+
let length = 0;
301+
for (const [k, v] of tabularData) {
302+
keys.push(inspect(k));
303+
values.push(inspect(v));
304+
length++;
305+
}
306+
return final([
307+
iterKey, keyKey, valuesKey
308+
], [
309+
getIndexArray(length),
310+
keys,
311+
values,
312+
]);
313+
}
314+
315+
const setIter = isSetIterator(tabularData);
316+
if (setIter)
317+
tabularData = previewSetIterator(tabularData);
318+
319+
const setlike = setIter || isSet(tabularData);
320+
if (setlike ||
321+
(properties === undefined &&
322+
(isArray(tabularData) || isTypedArray(tabularData)))) {
323+
const values = [];
324+
let length = 0;
325+
for (const v of tabularData) {
326+
values.push(inspect(v));
327+
length++;
328+
}
329+
return final([setlike ? iterKey : indexKey, valuesKey], [
330+
getIndexArray(length),
331+
values,
332+
]);
333+
}
334+
335+
const map = {};
336+
let hasPrimitives = false;
337+
const valuesKeyArray = [];
338+
const indexKeyArray = ObjectKeys(tabularData);
339+
340+
for (var i = 0; i < indexKeyArray.length; i++) {
341+
const item = tabularData[indexKeyArray[i]];
342+
const primitive = item === null ||
343+
(typeof item !== 'function' && typeof item !== 'object');
344+
if (properties === undefined && primitive) {
345+
hasPrimitives = true;
346+
valuesKeyArray[i] = inspect(item);
347+
} else {
348+
const keys = properties || ObjectKeys(item);
349+
for (const key of keys) {
350+
if (map[key] === undefined)
351+
map[key] = [];
352+
if ((primitive && properties) || !hasOwnProperty(item, key))
353+
map[key][i] = '';
354+
else
355+
map[key][i] = item == null ? item : inspect(item[key]);
356+
}
357+
}
358+
}
359+
360+
const keys = ObjectKeys(map);
361+
const values = ObjectValues(map);
362+
if (hasPrimitives) {
363+
keys.push(valuesKey);
364+
values.push(valuesKeyArray);
365+
}
366+
keys.unshift(indexKey);
367+
values.unshift(indexKeyArray);
368+
369+
return final(keys, values);
370+
};
371+
245372
module.exports = new Console(process.stdout, process.stderr);
246373
module.exports.Console = Console;
247374

lib/internal/cli_table.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use strict';
2+
3+
const { Buffer } = require('buffer');
4+
const { removeColors } = require('internal/util');
5+
const HasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
6+
7+
const tableChars = {
8+
/* eslint-disable node-core/non-ascii-character */
9+
middleMiddle: '─',
10+
rowMiddle: '┼',
11+
topRight: '┐',
12+
topLeft: '┌',
13+
leftMiddle: '├',
14+
topMiddle: '┬',
15+
bottomRight: '┘',
16+
bottomLeft: '└',
17+
bottomMiddle: '┴',
18+
rightMiddle: '┤',
19+
left: '│ ',
20+
right: ' │',
21+
middle: ' │ ',
22+
/* eslint-enable node-core/non-ascii-character */
23+
};
24+
25+
const countSymbols = (string) => {
26+
const normalized = removeColors(string).normalize('NFC');
27+
return Buffer.from(normalized, 'UCS-2').byteLength / 2;
28+
};
29+
30+
const renderRow = (row, columnWidths) => {
31+
let out = tableChars.left;
32+
for (var i = 0; i < row.length; i++) {
33+
const cell = row[i];
34+
const len = countSymbols(cell);
35+
const needed = (columnWidths[i] - len) / 2;
36+
// round(needed) + ceil(needed) will always add up to the amount
37+
// of spaces we need while also left justifying the output.
38+
out += `${' '.repeat(needed)}${cell}${' '.repeat(Math.ceil(needed))}`;
39+
if (i !== row.length - 1)
40+
out += tableChars.middle;
41+
}
42+
out += tableChars.right;
43+
return out;
44+
};
45+
46+
const table = (head, columns) => {
47+
const rows = [];
48+
const columnWidths = head.map((h) => countSymbols(h));
49+
const longestColumn = columns.reduce((n, a) => Math.max(n, a.length), 0);
50+
51+
for (var i = 0; i < head.length; i++) {
52+
const column = columns[i];
53+
for (var j = 0; j < longestColumn; j++) {
54+
if (!rows[j])
55+
rows[j] = [];
56+
const v = rows[j][i] = HasOwnProperty(column, j) ? column[j] : '';
57+
const width = columnWidths[i] || 0;
58+
const counted = countSymbols(v);
59+
columnWidths[i] = Math.max(width, counted);
60+
}
61+
}
62+
63+
const divider = columnWidths.map((i) =>
64+
tableChars.middleMiddle.repeat(i + 2));
65+
66+
const tl = tableChars.topLeft;
67+
const tr = tableChars.topRight;
68+
const lm = tableChars.leftMiddle;
69+
let result = `${tl}${divider.join(tableChars.topMiddle)}${tr}
70+
${renderRow(head, columnWidths)}
71+
${lm}${divider.join(tableChars.rowMiddle)}${tableChars.rightMiddle}
72+
`;
73+
74+
for (const row of rows)
75+
result += `${renderRow(row, columnWidths)}\n`;
76+
77+
result += `${tableChars.bottomLeft}${
78+
divider.join(tableChars.bottomMiddle)}${tableChars.bottomRight}`;
79+
80+
return result;
81+
};
82+
83+
module.exports = table;

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
'lib/zlib.js',
8282
'lib/internal/async_hooks.js',
8383
'lib/internal/buffer.js',
84+
'lib/internal/cli_table.js',
8485
'lib/internal/child_process.js',
8586
'lib/internal/cluster/child.js',
8687
'lib/internal/cluster/master.js',

0 commit comments

Comments
 (0)