1
|
|
|
"use strict"; |
2
|
|
|
|
3
|
|
|
const |
4
|
|
|
|
5
|
|
|
{ first, strlen } = require ('printable-characters'), // handles ANSI codes and invisible characters |
6
|
|
|
|
7
|
|
|
limit = (s, n) => (first (s, n - 1) + '…'), |
8
|
|
|
|
9
|
|
|
max = (a, b) => (a === undefined) ? b : ((b === undefined) ? a : Math.max (a, b)), |
10
|
|
|
|
11
|
|
|
arrmax = (a, b) => a.length > b.length ? a : b, |
|
|
|
|
12
|
|
|
|
13
|
|
|
asColumns = (rows, cfg_) => { |
14
|
|
|
|
15
|
|
|
const |
16
|
|
|
|
17
|
|
|
zip = (arrs, f) => arrs.reduce ((a, b) => b.map ((b, i) => [...a[i] || [], b]), []) |
18
|
|
|
.map (args => f (...args)), |
19
|
|
|
|
20
|
|
|
//dasd = zip ([[1,2,3], [4,5,6], [7,8,9], [10,11,12]], (...args) => console.log (args)), |
21
|
|
|
|
22
|
|
|
/* Convert cell data to string (converting multiline text to singleline) */ |
23
|
|
|
|
24
|
|
|
cells = rows.map (r => r.map (c => (c === undefined) ? '' : cfg_.print (c).replace (/\n/g, '\\n'))), |
25
|
|
|
|
26
|
|
|
/* Compute column widths (per row) and max widths (per column) */ |
27
|
|
|
|
28
|
|
|
cellWidths = cells.map (r => r.map (strlen)), |
29
|
|
|
maxWidths = zip (cellWidths, max), |
30
|
|
|
|
31
|
|
|
/* Default config */ |
32
|
|
|
|
33
|
|
|
cfg = Object.assign ({ |
34
|
|
|
delimiter: ' ', |
35
|
|
|
minColumnWidths: maxWidths.map (x => 0), |
|
|
|
|
36
|
|
|
maxTotalWidth: 0 }, cfg_), |
37
|
|
|
|
38
|
|
|
delimiterLength = strlen (cfg.delimiter), |
39
|
|
|
|
40
|
|
|
/* Project desired column widths, taking maxTotalWidth and minColumnWidths in account. */ |
41
|
|
|
|
42
|
|
|
totalWidth = maxWidths.reduce ((a, b) => a + b, 0), |
43
|
|
|
relativeWidths = maxWidths.map (w => w / totalWidth), |
44
|
|
|
maxTotalWidth = cfg.maxTotalWidth - (delimiterLength * (maxWidths.length - 1)), |
45
|
|
|
excessWidth = Math.max (0, totalWidth - maxTotalWidth), |
46
|
|
|
computedWidths = zip ([cfg.minColumnWidths, maxWidths, relativeWidths], |
47
|
|
|
(min, max, relative) => Math.max (min, Math.floor (max - excessWidth * relative))), |
48
|
|
|
|
49
|
|
|
/* This is how many symbols we should pad or cut (per column). */ |
50
|
|
|
|
51
|
|
|
restCellWidths = cellWidths.map (widths => zip ([computedWidths, widths], (a, b) => a - b)) |
52
|
|
|
|
53
|
|
|
/* Perform final composition. */ |
54
|
|
|
|
55
|
|
|
return zip ([cells, restCellWidths], (a, b) => |
56
|
|
|
zip ([a, b], (str, w) => (w >= 0) |
57
|
|
|
? (str + ' '.repeat (w)) |
58
|
|
|
: (limit (str, strlen (str) + w))).join (cfg.delimiter)) |
59
|
|
|
}, |
60
|
|
|
|
61
|
|
|
asTable = cfg => Object.assign (arr => { |
62
|
|
|
|
63
|
|
|
/* Print arrays */ |
64
|
|
|
|
65
|
|
|
if (arr[0] && Array.isArray (arr[0])) |
66
|
|
|
return asColumns (arr, cfg).join ('\n') |
|
|
|
|
67
|
|
|
|
68
|
|
|
/* Print objects */ |
69
|
|
|
|
70
|
|
|
const colNames = [...new Set ([].concat (...arr.map (Object.keys)))], |
71
|
|
|
columns = [colNames.map (cfg.title), ...arr.map (o => colNames.map (key => o[key]))], |
72
|
|
|
lines = asColumns (columns, cfg) |
73
|
|
|
|
74
|
|
|
return [lines[0], cfg.dash.repeat (strlen (lines[0])), ...lines.slice (1)].join ('\n') |
75
|
|
|
|
76
|
|
|
}, cfg, { |
77
|
|
|
|
78
|
|
|
configure: newConfig => asTable (Object.assign ({}, cfg, newConfig)), |
79
|
|
|
}) |
80
|
|
|
|
81
|
|
|
module.exports = asTable ({ |
82
|
|
|
|
83
|
|
|
maxTotalWidth: Number.MAX_SAFE_INTEGER, |
84
|
|
|
print: String, |
85
|
|
|
title: String, |
86
|
|
|
dash: '-' |
87
|
|
|
}) |
88
|
|
|
|
89
|
|
|
|
90
|
|
|
|