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

console: add table method #18137

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 @@ -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
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we had the discussion about using Primordials or not but as long as we do not really have anything decided there: shall we really do this right now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

until we come to consensus on that conversation i'm going to continue saving commonly messed-with methods


// Track amount of indentation required via `console.group()`.
const kGroupIndent = Symbol('groupIndent');

Expand Down Expand Up @@ -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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to have a identical behavior for Set, SetIteraror, Array, TypedArray.
All other types should be handled identical as Object.

The only difference necessary is the name for the index. That way the output becomes way more coherent.

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: add a special case in case no keys exist from the item and to show the item in the Values in that case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i assume you mean for non-arrays and non-objects?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I mean e.g. an empty array ([]) or empty object ({}) etc. Primitives are already handled above, so there is no point for those. That way it this will be visible instead of being invisible.

Another point: this will not work with Set / Map / SetIterator / MapIterator.

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