Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v9.x backport] console: add table method #19750

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions doc/api/console.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,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
Expand Down
129 changes: 128 additions & 1 deletion lib/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -241,6 +261,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;

Expand Down
83 changes: 83 additions & 0 deletions lib/internal/cli_table.js
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading