[v9.x backport] console: add table method by devsnek · Pull Request #19750 · nodejs/node · GitHub
Skip to content
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
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
Loading