UNPKG

4.23 kBJavaScriptView Raw
1'use strict'
2
3const stripAnsi = require('strip-ansi')
4const cli = require('..')
5
6/**
7 * Generates a Unicode table and feeds it into configured printer.
8 *
9 * Top-level arguments:
10 *
11 * @arg {Object[]} data - the records to format as a table.
12 * @arg {Object} options - configuration for the table.
13 *
14 * @arg {Object[]} [options.columns] - Options for formatting and finding values for table columns.
15 * @arg {function(string)} [options.headerAnsi] - Zero-width formattter for entire header.
16 * @arg {string} [options.colSep] - Separator between columns.
17 * @arg {function(row, options)} [options.after] - Function called after each row is printed.
18 * @arg {function(string)} [options.printLine] - Function responsible for printing to terminal.
19 * @arg {function(cells)} [options.printHeader] - Function to print header cells as a row.
20 * @arg {function(cells)} [options.printRow] - Function to print cells as a row.
21 *
22 * @arg {function(row)|string} [options.columns[].key] - Path to the value in the row or function to retrieve the pre-formatted value for the cell.
23 * @arg {function(string)} [options.columns[].label] - Header name for column.
24 * @arg {function(string, row)} [options.columns[].format] - Formatter function for column value.
25 * @arg {function(row)} [options.columns[].get] - Function to return a value to be presented in cell without formatting.
26 *
27 */
28function table (data, options) {
29 const ary = require('lodash.ary')
30 const defaults = require('lodash.defaults')
31 const get = require('lodash.get')
32 const identity = require('lodash.identity')
33 const keys = require('lodash.keys')
34 const noop = require('lodash.noop')
35 const partial = require('lodash.partial')
36 const property = require('lodash.property')
37 const repeat = require('lodash.repeat')
38 const result = require('lodash.result')
39
40 let _defaults = {
41 colSep: ' ',
42 after: noop,
43 headerAnsi: identity,
44 printLine: cli.log,
45 printRow: function (cells) {
46 this.printLine(cells.join(this.colSep).trimRight())
47 },
48 printHeader: function (cells) {
49 this.printRow(cells.map(ary(this.headerAnsi, 1)))
50 this.printRow(cells.map(hdr => hdr.replace(/./g, '─')))
51 }
52 }
53
54 let colDefaults = {
55 format: value => value ? value.toString() : '',
56 width: 0,
57 label: function () { return this.key.toString() },
58
59 get: function (row) {
60 let value
61 let path = result(this, 'key')
62
63 if (!path) {
64 value = row
65 } else {
66 value = get(row, path)
67 }
68
69 return this.format(value, row)
70 }
71 }
72
73 function calcWidth (cell) {
74 let lines = stripAnsi(cell).split(/[\r\n]+/)
75 let lineLengths = lines.map(property('length'))
76 return Math.max.apply(Math, lineLengths)
77 }
78
79 function pad (string, length) {
80 let visibleLength = stripAnsi(string).length
81 let diff = length - visibleLength
82
83 return string + repeat(' ', Math.max(0, diff))
84 }
85
86 function render () {
87 let columns = options.columns || keys(data[0] || {})
88
89 if (typeof columns[0] === 'string') {
90 columns = columns.map(key => ({key}))
91 }
92
93 let defaultsApplied = false
94 for (let row of data) {
95 row.height = 1
96 for (let col of columns) {
97 if (!defaultsApplied) defaults(col, colDefaults)
98
99 let cell = col.get(row)
100
101 col.width = Math.max(
102 result(col, 'label').length,
103 col.width,
104 calcWidth(cell)
105 )
106
107 row.height = Math.max(
108 row.height,
109 cell.split(/[\r\n]+/).length
110 )
111 }
112 defaultsApplied = true
113 }
114
115 if (options.printHeader) {
116 options.printHeader(columns.map(function (col) {
117 let label = result(col, 'label')
118 return pad(label, col.width || label.length)
119 }))
120 }
121
122 function getNthLineOfCell (n, row, col) {
123 // TODO memoize this
124 let lines = col.get(row).split(/[\r\n]+/)
125 return pad(lines[n] || '', col.width)
126 }
127
128 for (let row of data) {
129 for (let i = 0; i < row.height; i++) {
130 let cells = columns.map(partial(getNthLineOfCell, i, row))
131 options.printRow(cells)
132 }
133 options.after(row, options)
134 }
135 }
136
137 defaults(options, _defaults)
138 render()
139}
140
141module.exports = table