UNPKG

prettyjsontable

Version:

print json files as pretty table

204 lines (203 loc) 8.53 kB
import chalk from 'chalk'; import stringWidth from 'string-width'; import { Matrix } from './matrix.js'; // this function returns an array of parsed JSON objects, the function does not care about formating. i.e. `[1,2\n][3,4]\n { "a"\r\n:2}\r\r` is valid input // if you have a well formated line based JSONStream, you could use data.split("\n") instead, as that would be roughly 2x faster function JSONStreamToArray(data) { const atPositionRE = /Unexpected token . in JSON at position (\d*)/; const out = []; let pos = data.length; while (data.length > 0) { try { out.push(JSON.parse(data.slice(0, pos))); data = data.slice(pos); pos = data.length; } catch (e) { if (e instanceof Error) { const atPositionError = atPositionRE.exec(e.message); if (atPositionError != null) { pos = +atPositionError[1]; } else { throw e; } } else { throw e; } } } return out; } // this function handles the edge case where the input already is an array of objects export function dataToTable(data) { const tmp = JSONStreamToArray(data); // if we have only one element and this element is an array of objects, then we return it directly if (tmp.length === 1 && Array.isArray(tmp[0]) && typeof tmp[0][0] === 'object') { return tmp[0]; } return tmp; } /* 12.345 -> 3 */ const afterdotRE = /\.(\d*)$/; function getDecimalPlaces(v) { const afterDot = afterdotRE.exec(v.toString()); if (afterDot != null) { return afterDot[1].length; } return 0; } function transpose(m) { return m[0].map((x, i) => m.map((x) => x[i])); } export function prettyjsongraph(data, options) { const columns = new Map(); // if the input is a string, convert it to data if (typeof (data) === 'string') { data = dataToTable(data); } // extract column names (aka table header) and decimal places data.forEach(d => { Object.keys(d).forEach(column => { columns.set(column, column); }); }); // add table header data.unshift(Object.fromEntries(columns)); // convert array of objects to array of array (like excel ;) let table = data.map((e) => Array.from(columns.values()).map(column => e[column])); // filter and rearrange columns (if options.columns is set) if (Array.isArray(options.columns)) { table = table.map((row) => options.columns.map((i) => row[+i - 1])); } const header = table.shift(); table = transpose(table); return Matrix.drawGraph(table, header); // return [asciichart.plot(table as number[][], { height: 20, colors }), header.map((v, i) => asciichart.colored(v, colors[i])).join(' ')].join('\n') } export function prettyjsontable(data, options) { const columns = new Map(); const firstJSONDate = new Date(options.unixstart).valueOf(); const lastJSONDate = new Date(options.unixend).valueOf(); // if the input is a string, convert it to data if (typeof (data) === 'string') { data = dataToTable(data); } // extract column names (aka table header) and decimal places data.forEach(d => { Object.keys(d).forEach(column => { columns.set(column, column); }); }); // calculate column widths const decimalPlaces = reduceObjects(mapObjects(data, (column, value) => typeof (value) === 'number' ? getDecimalPlaces(value) : NaN), max); // add table header data.unshift(Object.fromEntries(columns)); // fill dummy object const dummy = mapObject(Object.fromEntries(columns), () => ''); // extend all objects with empty string const dataWithExtendedObjects = data.map(o => ({ ...dummy, ...o })); // convert all values to strings const dataWithValuesConvertedToStrings = mapObjects(dataWithExtendedObjects, (column, value) => convertValues(value, column).replace(/[\t\r\n]/g, '☐')); // calculate column widths const columnWidths = reduceObjects(mapObjects(dataWithValuesConvertedToStrings, (column, value) => stringWidth(value)), max); // pad all cells of one column to the same length const dataWithPaddedValues = mapObjects(dataWithValuesConvertedToStrings, (column, value) => pad(value, column)); // convert array of objects to array of array (like excel ;) let table = dataWithPaddedValues.map((e) => Array.from(columns.values()).map(column => e[column])); // filter and rearrange columns (if options.columns is set) if (Array.isArray(options.columns)) { table = table.map((row) => options.columns.map((i) => row[+i - 1])); } // join cells and rows return table.map((row) => ` ${row.join(chalk.blueBright(' | '))} `).map(colorizeLineBg).join('\n'); /// //////////////////////////////// internal sub-functions /////////////////////////////////// function convertValues(value, column) { if (typeof value === 'number') { if (value > firstJSONDate && value < lastJSONDate && options.msunixtime.length > 0) { return chalkOrValue(new Date(value).toISOString(), options.msunixtime); } if (value > firstJSONDate / 1000 && value < lastJSONDate / 1000 && options.unixtime.length > 0) { return chalkOrValue(new Date(value * 1000).toISOString(), options.unixtime); } const l = getDecimalPlaces(value); const maxl = decimalPlaces[column]; let s = value.toString(); if (!isNaN(maxl) && maxl > 0) { s = s + ' '.repeat(maxl - l); if (l === 0) { s = s + ' '; } } if (Number.isFinite(value) && value < 0) { return chalkOrValue(s, options.negative); } return chalkOrValue(s, options.number); } if (Array.isArray(value)) { return chalk.redBright.italic.bold('[Array]'); } if (typeof (value) === 'object') { return chalk.redBright.italic.bold('{Object}'); } if (typeof (value) === 'bigint') { return value.toString(); } if (typeof (value) === 'string') { return value; } if (typeof (value) === 'boolean') { if (!value && options.false.length > 0) { return chalkOrValue(value, options.false); } return chalkOrValue(value, options.boolean); } return ''; // null or undefined or function } function pad(value, column) { const l = decimalPlaces[column]; const w = columnWidths[column]; if (w !== undefined) { if (!isNaN(l)) { // this is a number column, so we do a right pad return (' '.repeat(w - stringWidth(value))) + value; } else { return value + (' '.repeat(w - stringWidth(value))); } } return 'PADERROR'; // js String.padEnd() does not account for double-half-width characters like "古" } function colorizeLineBg(line, lineNumber) { if (lineNumber === 0) { return chalkBgOrValue(line, options.header); } if (lineNumber % 2 === 0) { return chalkBgOrValue(line, options.even); } return chalkBgOrValue(line, options.odd); } } /// //////////////////////////////////////////////////////////////////// helpers /////////////////////////////////////////////////////////////////////////////// export function mapObject(obj, fun) { return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fun(k, v)])); } export function mapObjects(objs, fun) { return objs.map(obj => mapObject(obj, fun)); } export function max(a, b) { return a > b ? a : b; } export function min(a, b) { return a < b ? a : b; } export function reduceObjects(objects, fun) { const old = objects[0]; objects.slice(1).forEach(obj => { Object.keys(old).forEach(key => { old[key] = fun(obj[key], old[key], key); }); }); return old; } function chalkOrValue(value, color) { if (color !== undefined && color.length > 0) { return chalk.hex(color)(value); } return value.toString(); } function chalkBgOrValue(value, color) { if (color !== undefined && color.length > 0) { return chalk.bgHex(color)(value); } return value.toString(); }