diff options
author | Gus Caplan <me@gus.host> | 2018-01-13 13:55:19 -0600 |
---|---|---|
committer | Gus Caplan <me@gus.host> | 2018-03-30 19:41:41 -0500 |
commit | 97ace0449292eab3f044158ba3787e6af5dd6f3a (patch) | |
tree | cd6150997307934e3f1998252d3465709469916b /lib | |
parent | 83d44bee0134e3b8b6b98bf89e1f02981d72adee (diff) | |
download | node-new-97ace0449292eab3f044158ba3787e6af5dd6f3a.tar.gz |
console: add table method
PR-URL: https://github.com/nodejs/node/pull/18137
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/console.js | 129 | ||||
-rw-r--r-- | lib/internal/cli_table.js | 83 |
2 files changed, 211 insertions, 1 deletions
diff --git a/lib/console.js b/lib/console.js index d70a6b30b7..456c0cc439 100644 --- a/lib/console.js +++ b/lib/console.js @@ -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'); @@ -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)) { + 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; diff --git a/lib/internal/cli_table.js b/lib/internal/cli_table.js new file mode 100644 index 0000000000..4c07d92eeb --- /dev/null +++ b/lib/internal/cli_table.js @@ -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; |