tty-table 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/format.js DELETED
@@ -1,282 +0,0 @@
1
- const stripAnsi = require("strip-ansi")
2
- const smartwrap = require("smartwrap")
3
- const wcwidth = require("wcwidth")
4
-
5
- const addPadding = (config, width) => {
6
- return width + config.paddingLeft + config.paddingRight
7
- }
8
-
9
- /**
10
- * Returns the widest cell give a collection of rows
11
- *
12
- * @param object columnOptions
13
- * @param array rows
14
- * @param integer columnIndex
15
- * @returns string
16
- */
17
- const getMaxLength = (columnOptions, rows, columnIndex) => {
18
- let iterable
19
-
20
- // add header value, alias to calculate width when applicable
21
- if (columnOptions && (columnOptions.value || columnOptions.alias)) {
22
- // string we use from header
23
- let val = columnOptions.alias || columnOptions.value
24
- val = val.toString()
25
- // create a row with value in the current columnIndex
26
- const headerRow = Array(rows[0].length)
27
- headerRow[columnIndex] = val
28
- // add header row to new array we will check for max value width
29
- iterable = rows.slice()
30
- iterable.push(headerRow)
31
- } else {
32
- // no header value, just use rows to derive max width
33
- iterable = rows
34
- }
35
-
36
- const widest = iterable.reduce((prev, row) => {
37
- if (row[columnIndex]) {
38
- // check cell value is object or scalar
39
- const value = (row[columnIndex].value) ? row[columnIndex].value : row[columnIndex]
40
- const width = Math.max(
41
- ...stripAnsi(value.toString()).split(/[\n\r]/).map((s) => wcwidth(s))
42
- )
43
- return (width > prev) ? width : prev
44
- }
45
- return prev
46
- }, 0)
47
-
48
- return widest
49
- }
50
-
51
- /**
52
- * Get total width available to this table instance
53
- *
54
- *
55
- */
56
- const getAvailableWidth = config => {
57
- if (process && ((process.stdout && process.stdout.columns) || (process.env && process.env.COLUMNS))) {
58
- // forked calls that do not inherit process.stdout must use process.env
59
- let viewport = (process.stdout && process.stdout.columns) ? process.stdout.columns : process.env.COLUMNS
60
- viewport = viewport - config.marginLeft
61
-
62
- // table width percentage of (viewport less margin)
63
- if (config.width !== "auto" && /^\d+%$/.test(config.width)) {
64
- return Math.min(1, (config.width.slice(0, -1) * 0.01)) * viewport
65
- }
66
-
67
- // table width fixed
68
- if (config.width !== "auto" && /^\d+$/.test(config.width)) {
69
- config.FIXED_WIDTH = true
70
- return config.width
71
- }
72
-
73
- // table width equals viewport less margin
74
- // @TODO deprecate and remove "auto", which was never documented so should not be
75
- // an issue
76
- return viewport
77
- }
78
-
79
- // browser
80
- /* istanbul ignore next */
81
- if (typeof window !== "undefined") return window.innerWidth // eslint-disable-line
82
-
83
- // process.stdout.columns does not exist. assume redirecting to write stream
84
- // use 80 columns, which is VT200 standard
85
- return config.COLUMNS - config.marginLeft
86
- }
87
-
88
- module.exports.getStringLength = string => {
89
- // stripAnsi(string.replace(/[^\x00-\xff]/g,'XX')).length
90
- return wcwidth(stripAnsi(string))
91
- }
92
-
93
- module.exports.wrapCellText = (
94
- config,
95
- cellValue,
96
- columnIndex,
97
- cellOptions,
98
- rowType
99
- ) => {
100
- // ANSI chararacters that demarcate the start/end of a line
101
- const startAnsiRegexp = /^(\033\[[0-9;]*m)+/
102
- const endAnsiRegexp = /(\033\[[0-9;]*m)+$/
103
-
104
- // coerce cell value to string
105
- let string = cellValue.toString()
106
-
107
- // store matching ANSI characters
108
- const startMatches = string.match(startAnsiRegexp) || [""]
109
-
110
- // remove ANSI start-of-line chars
111
- string = string.replace(startAnsiRegexp, "")
112
-
113
- // store matching ANSI characters so can be later re-attached
114
- const endMatches = string.match(endAnsiRegexp) || [""]
115
-
116
- // remove ANSI end-of-line chars
117
- string = string.replace(endAnsiRegexp, "")
118
-
119
- let alignTgt
120
-
121
- switch (rowType) {
122
- case ("header"):
123
- alignTgt = "headerAlign"
124
- break
125
- case ("body"):
126
- alignTgt = "align"
127
- break
128
- default:
129
- alignTgt = "footerAlign"
130
- }
131
-
132
- // equalize padding for centered lines
133
- if (cellOptions[alignTgt] === "center") {
134
- cellOptions.paddingLeft = cellOptions.paddingRight = Math.max(
135
- cellOptions.paddingRight,
136
- cellOptions.paddingLeft,
137
- 0
138
- )
139
- }
140
-
141
- const columnWidth = config.table.columnWidths[columnIndex]
142
-
143
- // innerWidth is the width available for text within the cell
144
- const innerWidth = columnWidth
145
- - cellOptions.paddingLeft
146
- - cellOptions.paddingRight
147
- - config.GUTTER
148
-
149
- if (typeof config.truncate === "string") {
150
- string = exports.truncate(string, cellOptions, innerWidth)
151
- } else {
152
- string = exports.wrap(string, cellOptions, innerWidth)
153
- }
154
-
155
- // format each line
156
- const cell = string.split("\n").map(line => {
157
- line = line.trim()
158
-
159
- const lineLength = exports.getStringLength(line)
160
-
161
- // alignment
162
- if (lineLength < columnWidth) {
163
- let emptySpace = columnWidth - lineLength
164
-
165
- switch (true) {
166
- case (cellOptions[alignTgt] === "center"):
167
- emptySpace--
168
- const padBoth = Math.floor(emptySpace / 2)
169
- const padRemainder = emptySpace % 2
170
- line = Array(padBoth + 1).join(" ")
171
- + line
172
- + Array(padBoth + 1 + padRemainder).join(" ")
173
- break
174
-
175
- case (cellOptions[alignTgt] === "right"):
176
- line = Array(emptySpace - cellOptions.paddingRight).join(" ")
177
- + line
178
- + Array(cellOptions.paddingRight + 1).join(" ")
179
- break
180
-
181
- default:
182
- line = Array(cellOptions.paddingLeft + 1).join(" ")
183
- + line
184
- + Array(emptySpace - cellOptions.paddingLeft).join(" ")
185
- }
186
- }
187
-
188
- // put ANSI color codes BACK on the beginning and end of string
189
- return startMatches[0] + line + endMatches[0]
190
- })
191
-
192
- return { cell, innerWidth }
193
- }
194
-
195
- module.exports.truncate = (string, cellOptions, maxWidth) => {
196
- const stringWidth = wcwidth(string)
197
-
198
- if (maxWidth < stringWidth) {
199
- // @TODO give user option to decide if they want to break words on wrapping
200
- string = smartwrap(string, {
201
- width: maxWidth - cellOptions.truncate.length,
202
- breakword: true
203
- }).split("\n")[0]
204
- string = string + cellOptions.truncate
205
- }
206
-
207
- return string
208
- }
209
-
210
- module.exports.wrap = (string, cellOptions, innerWidth) => {
211
- const outstring = smartwrap(string, {
212
- errorChar: cellOptions.defaultErrorValue,
213
- minWidth: 1,
214
- trim: true,
215
- width: innerWidth
216
- })
217
-
218
- return outstring
219
- }
220
-
221
- module.exports.getColumnWidths = (config, rows) => {
222
- const availableWidth = getAvailableWidth(config)
223
-
224
- // iterate over the header if we have it, iterate over the first row
225
- // if we do not (to step through the correct number of columns)
226
- const iterable = (config.table.header[0] && config.table.header[0].length > 0)
227
- ? config.table.header[0] : rows[0]
228
-
229
- let widths = iterable.map((column, columnIndex) => {
230
- let result
231
-
232
- switch (true) {
233
- // column width is a percentage of table width specified in column header
234
- case (typeof column === "object" && (/^\d+%$/.test(column.width))):
235
- result = (column.width.slice(0, -1) * 0.01) * availableWidth
236
- result = addPadding(config, result)
237
- break
238
-
239
- // column width is specified in column header
240
- case (typeof column === "object" && (/^\d+$/.test(column.width))):
241
- result = column.width
242
- break
243
-
244
- // 'auto' sets column width to its longest value in the initial data set
245
- default:
246
- const columnOptions = (config.table.header[0][columnIndex])
247
- ? config.table.header[0][columnIndex] : {}
248
- const measurableRows = (rows.length) ? rows : config.table.header[0]
249
-
250
- result = getMaxLength(columnOptions, measurableRows, columnIndex)
251
-
252
- // add spaces for padding if not centered
253
- // @TODO test with if not centered conditional
254
- result = addPadding(config, result)
255
- }
256
-
257
- // add space for gutter
258
- result = result + config.GUTTER
259
- return result
260
- })
261
-
262
- // calculate sum of all column widths (including marginLeft)
263
- const totalWidth = widths.reduce((prev, current) => prev + current)
264
-
265
- // proportionately resize columns when necessary
266
- if (totalWidth > availableWidth || config.FIXED_WIDTH) {
267
- // proportion wont be exact fit, but this method keeps us safe
268
- const proportion = (availableWidth / totalWidth).toFixed(2) - 0.01
269
- const relativeWidths = widths.map(value => Math.max(2, Math.floor(proportion * value)))
270
- if (config.FIXED_WIDTH) return relativeWidths
271
-
272
- // when proportion < 0 column cant be resized and totalWidth must overflow viewport
273
- if (proportion > 0) {
274
- const totalRelativeWidths = relativeWidths.reduce((prev, current) => prev + current)
275
- widths = (totalRelativeWidths < totalWidth) ? relativeWidths : widths
276
- }
277
- } else {
278
- widths = widths.map(Math.floor)
279
- }
280
-
281
- return widths
282
- }
package/src/main.js DELETED
@@ -1,8 +0,0 @@
1
- if (require.main === module) {
2
- // called directly in terminal
3
- /* istanbul ignore next */
4
- require("./../adapters/terminal-adapter.js")
5
- } else {
6
- // called as a module
7
- module.exports = require("./../adapters/default-adapter.js")
8
- }
package/src/render.js DELETED
@@ -1,411 +0,0 @@
1
- const Style = require("./style.js")
2
- const Format = require("./format.js")
3
- const stripAnsi = require("strip-ansi")
4
-
5
- /**
6
- * Converts arrays of data into arrays of cell strings
7
- * @param {TtyTable.Config} config
8
- * @param {Array<Array<string>|object|TtyTable.Formatter>} inputData
9
- * @returns {Array<string>}
10
- */
11
- module.exports.stringifyData = (config, inputData) => {
12
- const sections = {
13
- header: [],
14
- body: [],
15
- footer: []
16
- }
17
- const marginLeft = Array(config.marginLeft + 1).join(" ")
18
- const borderStyle = config.borderCharacters[config.borderStyle]
19
- const borders = []
20
-
21
- // support backwards compatibility cli-table's multiple constructor geometries
22
- // @TODO deprecate and support only a single format
23
- const constructorType = exports.getConstructorGeometry(inputData[0] || [], config)
24
- const rows = exports.coerceConstructorGeometry(config, inputData, constructorType)
25
-
26
- // when streaming values to tty-table, we don't want column widths to change
27
- // from one rows set to the next, so we save the first set of widths and reuse
28
- if (!global.columnWidths) {
29
- global.columnWidths = {}
30
- }
31
-
32
- if (global.columnWidths[config.tableId]) {
33
- config.table.columnWidths = global.columnWidths[config.tableId]
34
- } else {
35
- const formattedRows = rows.map((row, rowIndex) => {
36
- return row.map((cell, cellIndex) => {
37
- return exports.buildCell(config, cell, cellIndex, "body", rowIndex, rows, inputData, true)
38
- })
39
- })
40
- global.columnWidths[config.tableId] = config.table.columnWidths = Format.getColumnWidths(config, formattedRows)
41
- }
42
-
43
- // stringify header cells
44
- // hide header if no column names or if specified in config
45
- switch (true) {
46
- case (config.showHeader !== null && !config.showHeader): // explicitly false, hide
47
- sections.header = []
48
- break
49
-
50
- case (config.showHeader === true): // explicitly true, show
51
- case (!!config.table.header[0].find(obj => obj.value || obj.alias)): // atleast one named column, show header
52
- sections.header = config.table.header.map(row => {
53
- return exports.buildRow(config, row, "header", null, rows, inputData)
54
- })
55
- break
56
-
57
- default: // no named columns, hide
58
- sections.header = []
59
- }
60
-
61
- // stringify body cells
62
- sections.body = rows.map((row, rowIndex) => {
63
- return exports.buildRow(config, row, "body", rowIndex, rows, inputData)
64
- })
65
-
66
- // stringify footer cells
67
- sections.footer = (config.table.footer instanceof Array && config.table.footer.length > 0) ? [config.table.footer] : []
68
-
69
- sections.footer = sections.footer.map(row => {
70
- return exports.buildRow(config, row, "footer", null, rows, inputData)
71
- })
72
-
73
- // apply borders
74
- // 0=top, 1=middle, 2=bottom
75
- for (let a = 0; a < 3; a++) {
76
- // add left border
77
- borders[a] = borderStyle[a].l
78
-
79
- // add joined borders for each column
80
- config.table.columnWidths.forEach((columnWidth, index, arr) => {
81
- // Math.max because otherwise columns 1 wide wont have horizontal border
82
- borders[a] += Array(Math.max(columnWidth, 2)).join(borderStyle[a].h)
83
- borders[a] += ((index + 1 < arr.length) ? borderStyle[a].j : "")
84
- })
85
-
86
- // add right border
87
- borders[a] += borderStyle[a].r
88
-
89
- // no trailing space on footer
90
- borders[a] = (a < 2) ? `${marginLeft + borders[a]}\n` : marginLeft + borders[a]
91
- }
92
-
93
- // top horizontal border
94
- let output = borders[0]
95
-
96
- // for each section (header,body,footer)
97
- Object.keys(sections).forEach((p, i) => {
98
- // for each row in the section
99
- while (sections[p].length) {
100
- const row = sections[p].shift()
101
-
102
- // if(row.length === 0) {break}
103
-
104
- row.forEach(line => {
105
- // vertical row borders
106
- output = `${output
107
- + marginLeft
108
- // left vertical border
109
- + borderStyle[1].v
110
- // join cells on vertical border
111
- + line.join(borderStyle[1].v)
112
- // right vertical border
113
- + borderStyle[1].v
114
- // end of line
115
- }\n`
116
- })
117
-
118
- // bottom horizontal row border
119
- switch (true) {
120
- // skip if end of body and no footer
121
- case (sections[p].length === 0
122
- && i === 1
123
- && sections.footer.length === 0):
124
- break
125
-
126
- // skip if end of footer
127
- case (sections[p].length === 0
128
- && i === 2):
129
- break
130
-
131
- // skip if compact
132
- case (config.compact && p === "body" && !row.empty):
133
- break
134
-
135
- // skip if border style is "none"
136
- case (config.borderStyle === "none" && config.compact):
137
- break
138
-
139
- default:
140
- output += borders[1]
141
- }
142
- }
143
- })
144
-
145
- // bottom horizontal border
146
- output += borders[2]
147
-
148
- const finalOutput = Array(config.marginTop + 1).join("\n") + output
149
-
150
- // record the height of the output
151
- config.height = finalOutput.split(/\r\n|\r|\n/).length
152
-
153
- return finalOutput
154
- }
155
-
156
- module.exports.buildRow = (config, row, rowType, rowIndex, rowData, inputData) => {
157
- let minRowHeight = 0
158
-
159
- // tag row as empty if empty, used for `compact` option
160
- if (row.length === 0 && config.compact) {
161
- row.empty = true
162
- return row
163
- }
164
-
165
- // force row to have correct number of columns
166
- const lengthDifference = config.table.columnWidths.length - row.length
167
- if (lengthDifference > 0) {
168
- // array (row) lacks elements, add until equal
169
- row = row.concat(Array.apply(null, new Array(lengthDifference)).map(() => null))
170
- } else if (lengthDifference < 0) {
171
- // array (row) has too many elements, remove until equal
172
- row.length = config.table.columnWidths.length
173
- }
174
-
175
- // convert each element in row to cell format
176
- row = row.map((elem, elemIndex) => {
177
- const cell = exports.buildCell(config, elem, elemIndex, rowType, rowIndex, rowData, inputData)
178
- minRowHeight = (minRowHeight < cell.length) ? cell.length : minRowHeight
179
- return cell
180
- })
181
-
182
- // apply top and bottom padding to row
183
- minRowHeight = (rowType === "header") ? minRowHeight
184
- : minRowHeight + (config.paddingBottom + config.paddingTop)
185
-
186
- const linedRow = Array.apply(null, { length: minRowHeight })
187
- .map(Function.call, () => [])
188
-
189
- row.forEach(function (cell, a) {
190
- const whitespace = Array(config.table.columnWidths[a]).join(" ")
191
-
192
- if (rowType === "body") {
193
- // add whitespace for top padding
194
- for (let i = 0; i < config.paddingTop; i++) {
195
- cell.unshift(whitespace)
196
- }
197
-
198
- // add whitespace for bottom padding
199
- for (let i = 0; i < config.paddingBottom; i++) {
200
- cell.push(whitespace)
201
- }
202
- }
203
-
204
- // a `row` is divided by columns (horizontally)
205
- // a `linedRow` becomes the row divided instead into an array of vertical lines
206
- // each nested line divided by columns
207
- for (let i = 0; i < minRowHeight; i++) {
208
- linedRow[i].push((typeof cell[i] !== "undefined")
209
- ? cell[i] : whitespace)
210
- }
211
- })
212
-
213
- return linedRow
214
- }
215
-
216
- module.exports.buildCell = (config, elem, columnIndex, rowType, rowIndex, rowData, inputData, dryRun = false) => {
217
- let cellValue = null
218
-
219
- const cellOptions = Object.assign(
220
- { reset: false },
221
- config,
222
- (rowType !== "header") ? config.columnSettings[columnIndex] : {},
223
- (typeof elem === "object") ? elem : {}
224
- )
225
-
226
- if (rowType === "header") {
227
- config.table.columns.push(cellOptions)
228
- cellValue = cellOptions.alias || cellOptions.value || ""
229
- } else {
230
- // set cellValue
231
- switch (true) {
232
- case (typeof elem === "undefined" || elem === null):
233
- // replace undefined/null elem values with placeholder
234
- cellValue = (config.errorOnNull) ? config.defaultErrorValue : config.defaultValue
235
- if (!Style.isColorEnabled()) {
236
- cellValue = stripAnsi(cellValue)
237
- }
238
- // @TODO add to elem defaults
239
- cellOptions.isNull = true
240
- break
241
-
242
- case (typeof elem === "object" && elem !== null && typeof elem.value !== "undefined"):
243
- cellValue = elem.value
244
- break
245
-
246
- case (typeof elem === "function"):
247
- cellValue = elem.bind({
248
- configure: function (object) {
249
- return Object.assign(cellOptions, object)
250
- },
251
- style: Style.style,
252
- resetStyle: Style.resetStyle
253
- })(
254
- cellValue,
255
- columnIndex,
256
- rowIndex,
257
- rowData,
258
- inputData
259
- )
260
- break
261
-
262
- default:
263
- // elem is assumed to be a scalar
264
- cellValue = elem
265
- }
266
-
267
- // run formatter
268
- if (rowType === "body" && typeof cellOptions.formatter === "function") {
269
- cellValue = cellOptions.formatter
270
- .bind({
271
- configure: function (object) {
272
- return Object.assign(cellOptions, object)
273
- },
274
- style: Style.style,
275
- resetStyle: Style.resetStyle
276
- })(
277
- cellValue,
278
- columnIndex,
279
- rowIndex,
280
- rowData,
281
- inputData
282
- )
283
- }
284
-
285
- if (dryRun) {
286
- return cellValue
287
- }
288
- }
289
-
290
- // colorize cellValue
291
- // we don't want the formatter to pass a styled cell value with ANSI codes
292
- // (in case user wants to do math or string operations to cell value), so
293
- // we apply default styles to the cell after it runs through the formatter
294
- // and omit those default styles if the user applied `this.resetStyle`
295
- if (!cellOptions.reset) {
296
- cellValue = Style.colorizeCell(cellValue, cellOptions, rowType)
297
- }
298
-
299
- // textwrap cellValue
300
- const { cell, innerWidth } = Format.wrapCellText(cellOptions, cellValue, columnIndex, cellOptions, rowType)
301
-
302
- if (rowType === "header") {
303
- config.table.columnInnerWidths.push(innerWidth)
304
- }
305
-
306
- return cell
307
- }
308
-
309
- /**
310
- * Check for a backwards compatible (cli-table) constructor
311
- */
312
- module.exports.getConstructorGeometry = (row, config) => {
313
- let type
314
-
315
- // rows passed as an object
316
- if (typeof row === "object" && !(row instanceof Array)) {
317
- const keys = Object.keys(row)
318
-
319
- if (config.adapter === "automattic") {
320
- // detected cross table
321
- const key = keys[0]
322
-
323
- if (row[key] instanceof Array) {
324
- type = "automattic-cross"
325
- } else {
326
- // detected vertical table
327
- type = "automattic-vertical"
328
- }
329
- } else {
330
- // detected horizontal table
331
- type = "o-horizontal"
332
- }
333
- } else {
334
- // rows passed as an array
335
- type = "a-horizontal"
336
- }
337
-
338
- return type
339
- }
340
-
341
- /**
342
- * Coerce backwards compatible constructor styles
343
- */
344
- module.exports.coerceConstructorGeometry = (config, rows, constructorType) => {
345
- let output = []
346
- switch (constructorType) {
347
- case ("automattic-cross"):
348
- // assign header styles to first column
349
- config.columnSettings[0] = config.columnSettings[0] || {}
350
- config.columnSettings[0].color = config.headerColor
351
-
352
- output = rows.map(obj => {
353
- const arr = []
354
- const key = Object.keys(obj)[0]
355
- arr.push(key)
356
- return arr.concat(obj[key])
357
- })
358
- break
359
-
360
- case ("automattic-vertical"):
361
- // assign header styles to first column
362
- config.columnSettings[0] = config.columnSettings[0] || {}
363
- config.columnSettings[0].color = config.headerColor
364
-
365
- output = rows.map(function (value) {
366
- const key = Object.keys(value)[0]
367
- return [key, value[key]]
368
- })
369
- break
370
-
371
- case ("o-horizontal"):
372
- // cell property names are specified in header columns
373
- if (config.table.header[0].length
374
- && config.table.header[0].every(obj => obj.value)) {
375
- output = rows.map(row => config.table.header[0]
376
- .map(obj => row[obj.value]))
377
- } // eslint-disable-line brace-style
378
- // no property names given, default to object property order
379
- else {
380
- output = rows.map(obj => Object.values(obj))
381
- }
382
- break
383
-
384
- case ("a-horizontal"):
385
- output = rows
386
- break
387
-
388
- default:
389
- }
390
-
391
- return output
392
- }
393
-
394
- // @TODO For rotating horizontal data into a vertical table
395
- // assumes all rows are same length
396
- // module.exports.verticalizeMatrix = (config, inputArray) => {
397
- //
398
- // // grow to # arrays equal to number of columns in input array
399
- // let outputArray = []
400
- // let headers = config.table.columns
401
- //
402
- // // create a row for each heading, and prepend the row
403
- // // with the heading name
404
- // headers.forEach(name => outputArray.push([name]))
405
- //
406
- // inputArray.forEach(row => {
407
- // row.forEach((element, index) => outputArray[index].push(element))
408
- // })
409
- //
410
- // return outputArray
411
- // }