UNPKG

10.2 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const command_1 = require("@oclif/command");
5const screen_1 = require("@oclif/screen");
6const chalk_1 = tslib_1.__importDefault(require("chalk"));
7const _ = tslib_1.__importStar(require("lodash"));
8const util_1 = require("util");
9const sw = require('string-width');
10const { orderBy } = require('natural-orderby');
11class Table {
12 constructor(data, columns, options = {}) {
13 this.data = data;
14 // assign columns
15 this.columns = Object.keys(columns).map((key) => {
16 const col = columns[key];
17 const extended = col.extended || false;
18 const get = col.get || ((row) => row[key]);
19 const header = typeof col.header === 'string' ? col.header : _.capitalize(key.replace(/\_/g, ' '));
20 const minWidth = Math.max(col.minWidth || 0, sw(header) + 1);
21 return {
22 extended,
23 get,
24 header,
25 key,
26 minWidth,
27 };
28 });
29 // and filter columns
30 if (options.columns) {
31 let filters = options.columns.split(',');
32 this.columns = this.filterColumnsFromHeaders(filters);
33 }
34 else if (!options.extended) {
35 // show extented columns/properties
36 this.columns = this.columns.filter(c => !c.extended);
37 }
38 // assign options
39 const { columns: cols, filter, csv, extended, sort, printLine } = options;
40 this.options = {
41 columns: cols,
42 csv,
43 extended,
44 filter,
45 'no-header': options['no-header'] || false,
46 'no-truncate': options['no-truncate'] || false,
47 printLine: printLine || ((s) => process.stdout.write(s + '\n')),
48 sort,
49 };
50 }
51 display() {
52 // build table rows from input array data
53 let rows = this.data.map(d => {
54 let row = {};
55 for (let col of this.columns) {
56 let val = col.get(d);
57 if (typeof val !== 'string')
58 val = util_1.inspect(val, { breakLength: Infinity });
59 row[col.key] = val;
60 }
61 return row;
62 });
63 // filter rows
64 if (this.options.filter) {
65 let [header, regex] = this.options.filter.split('=');
66 const isNot = header[0] === '-';
67 if (isNot)
68 header = header.substr(1);
69 let col = this.findColumnFromHeader(header);
70 if (!col || !regex)
71 throw new Error('Filter flag has an invalid value');
72 rows = rows.filter((d) => {
73 let re = new RegExp(regex);
74 let val = d[col.key];
75 let match = val.match(re);
76 return isNot ? !match : match;
77 });
78 }
79 // sort rows
80 if (this.options.sort) {
81 let sorters = this.options.sort.split(',');
82 let sortHeaders = sorters.map(k => k[0] === '-' ? k.substr(1) : k);
83 let sortKeys = this.filterColumnsFromHeaders(sortHeaders).map(c => {
84 return ((v) => v[c.key]);
85 });
86 let sortKeysOrder = sorters.map(k => k[0] === '-' ? 'desc' : 'asc');
87 rows = orderBy(rows, sortKeys, sortKeysOrder);
88 }
89 this.data = rows;
90 if (this.options.csv)
91 this.outputCSV();
92 else
93 this.outputTable();
94 }
95 findColumnFromHeader(header) {
96 return this.columns.find(c => c.header.toLowerCase() === header.toLowerCase());
97 }
98 filterColumnsFromHeaders(filters) {
99 // unique
100 filters = [...(new Set(filters))];
101 let cols = [];
102 filters.forEach(f => {
103 let c = this.columns.find(c => c.header.toLowerCase() === f.toLowerCase());
104 if (c)
105 cols.push(c);
106 });
107 return cols;
108 }
109 outputCSV() {
110 // tslint:disable-next-line:no-this-assignment
111 const { data, columns, options } = this;
112 options.printLine(columns.map(c => c.header).join(','));
113 data.forEach((d) => {
114 let row = [];
115 columns.forEach(col => row.push(d[col.key] || ''));
116 options.printLine(row.join(','));
117 });
118 }
119 outputTable() {
120 // tslint:disable-next-line:no-this-assignment
121 const { data, columns, options } = this;
122 // column truncation
123 //
124 // find max width for each column
125 for (let col of columns) {
126 // convert multi-line cell to single longest line
127 // for width calculations
128 let widthData = data.map((row) => {
129 let d = row[col.key];
130 let manyLines = d.split('\n');
131 if (manyLines.length > 1) {
132 return '*'.repeat(Math.max(...manyLines.map((r) => sw(r))));
133 }
134 return d;
135 });
136 const widths = ['.'.padEnd(col.minWidth - 1), col.header, ...widthData.map((row) => row)].map(r => sw(r));
137 col.maxWidth = Math.max(...widths) + 1;
138 col.width = col.maxWidth;
139 }
140 // terminal width
141 const maxWidth = screen_1.stdtermwidth;
142 // truncation logic
143 const shouldShorten = () => {
144 // don't shorten if full mode
145 if (options['no-truncate'] || !process.stdout.isTTY)
146 return;
147 // don't shorten if there is enough screen width
148 let dataMaxWidth = _.sumBy(columns, c => c.width);
149 let overWidth = dataMaxWidth - maxWidth;
150 if (overWidth <= 0)
151 return;
152 // not enough room, short all columns to minWidth
153 for (let col of columns) {
154 col.width = col.minWidth;
155 }
156 // if sum(minWidth's) is greater than term width
157 // nothing can be done so
158 // display all as minWidth
159 let dataMinWidth = _.sumBy(columns, c => c.minWidth);
160 if (dataMinWidth >= maxWidth)
161 return;
162 // some wiggle room left, add it back to "needy" columns
163 let wiggleRoom = maxWidth - dataMinWidth;
164 let needyCols = _.sortBy(columns.map(c => ({ key: c.key, needs: c.maxWidth - c.width })), c => c.needs);
165 for (let { key, needs } of needyCols) {
166 if (!needs)
167 continue;
168 let col = _.find(columns, c => (key === c.key));
169 if (!col)
170 continue;
171 if (wiggleRoom > needs) {
172 col.width = col.width + needs;
173 wiggleRoom = wiggleRoom - needs;
174 }
175 else if (wiggleRoom) {
176 col.width = col.width + wiggleRoom;
177 wiggleRoom = 0;
178 }
179 }
180 };
181 shouldShorten();
182 // print headers
183 if (!options['no-header']) {
184 let headers = '';
185 for (let col of columns) {
186 let header = col.header;
187 headers += header.padEnd(col.width);
188 }
189 options.printLine(chalk_1.default.bold(headers));
190 }
191 // print rows
192 for (let row of data) {
193 // find max number of lines
194 // for all cells in a row
195 // with multi-line strings
196 let numOfLines = 1;
197 for (let col of columns) {
198 const d = row[col.key];
199 let lines = d.split('\n').length;
200 if (lines > numOfLines)
201 numOfLines = lines;
202 }
203 let linesIndexess = [...Array(numOfLines).keys()];
204 // print row
205 // including multi-lines
206 linesIndexess.forEach((i) => {
207 let l = '';
208 for (let col of columns) {
209 const width = col.width;
210 let d = row[col.key];
211 d = d.split('\n')[i] || '';
212 const visualWidth = sw(d);
213 const colorWidth = (d.length - visualWidth);
214 let cell = d.padEnd(width + colorWidth);
215 if ((cell.length - colorWidth) > width || visualWidth === width) {
216 cell = cell.slice(0, width - 2) + '… ';
217 }
218 l += cell;
219 }
220 options.printLine(l);
221 });
222 }
223 }
224}
225function table(data, columns, options = {}) {
226 new Table(data, columns, options).display();
227}
228exports.table = table;
229(function (table) {
230 table.Flags = {
231 columns: command_1.flags.string({ exclusive: ['extended'], description: 'only show provided columns (comma-separated)' }),
232 sort: command_1.flags.string({ description: 'property to sort by (prepend \'-\' for descending)' }),
233 filter: command_1.flags.string({ description: 'filter property by partial string matching, ex: name=foo' }),
234 csv: command_1.flags.boolean({ exclusive: ['no-truncate', 'no-header'], description: 'output is csv format' }),
235 extended: command_1.flags.boolean({ exclusive: ['columns'], char: 'x', description: 'show extra columns' }),
236 'no-truncate': command_1.flags.boolean({ exclusive: ['csv'], description: 'do not truncate output to fit screen' }),
237 'no-header': command_1.flags.boolean({ exclusive: ['csv'], description: 'hide table header from output' }),
238 };
239 function flags(opts) {
240 if (opts) {
241 let f = {};
242 let o = (opts.only && typeof opts.only === 'string' ? [opts.only] : opts.only) || Object.keys(table.Flags);
243 let e = (opts.except && typeof opts.except === 'string' ? [opts.except] : opts.except) || [];
244 o.forEach((key) => {
245 if (e.includes(key))
246 return;
247 f[key] = table.Flags[key];
248 });
249 return f;
250 }
251 return table.Flags;
252 }
253 table.flags = flags;
254})(table = exports.table || (exports.table = {}));