Skip to content

Commit 789279b

Browse files
devsnekBridgeAR
authored andcommitted
console: add table method
PR-URL: nodejs#18137 Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
1 parent a85a08a commit 789279b

5 files changed

Lines changed: 420 additions & 1 deletion

File tree

doc/api/console.md

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

363363
See [`util.format()`][] for more information.
364364

365+
### console.table(tabularData[, properties])
366+
<!-- YAML
367+
added: REPLACEME
368+
-->
369+
370+
* `tabularData` {any}
371+
* `properties` {string[]} Alternate properties for constructing the table.
372+
373+
Try to construct a table with the columns of the properties of `tabularData`
374+
(or use `properties`) and rows of `tabularData` and logit. Falls back to just
375+
logging the argument if it can’t be parsed as tabular.
376+
377+
```js
378+
// These can't be parsed as tabular data
379+
console.table(Symbol());
380+
// Symbol()
381+
382+
console.table(undefined);
383+
// undefined
384+
```
385+
386+
```js
387+
console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }]);
388+
// ┌─────────┬─────┬─────┐
389+
// │ (index) │ a │ b │
390+
// ├─────────┼─────┼─────┤
391+
// │ 0 │ 1 │ 'Y' │
392+
// │ 1 │ 'Z' │ 2 │
393+
// └─────────┴─────┴─────┘
394+
```
395+
396+
```js
397+
console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }], ['a']);
398+
// ┌─────────┬─────┐
399+
// │ (index) │ a │
400+
// ├─────────┼─────┤
401+
// │ 0 │ 1 │
402+
// │ 1 │ 'Z' │
403+
// └─────────┴─────┘
404+
```
405+
365406
### console.time(label)
366407
<!-- YAML
367408
added: v0.1.104

lib/console.js

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,30 @@
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 { Buffer: { isBuffer } } = require('buffer');
32+
const cliTable = require('internal/cli_table');
2833
const util = require('util');
34+
const {
35+
isTypedArray, isSet, isMap,
36+
} = util.types;
2937
const kCounts = Symbol('counts');
3038

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

@@ -241,6 +260,105 @@ Console.prototype.groupEnd = function groupEnd() {
241260
this[kGroupIndent].slice(0, this[kGroupIndent].length - 2);
242261
};
243262

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

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
@@ -82,6 +82,7 @@
8282
'lib/zlib.js',
8383
'lib/internal/async_hooks.js',
8484
'lib/internal/buffer.js',
85+
'lib/internal/cli_table.js',
8586
'lib/internal/child_process.js',
8687
'lib/internal/cluster/child.js',
8788
'lib/internal/cluster/master.js',

0 commit comments

Comments
 (0)