summaryrefslogtreecommitdiff
path: root/deps/npm/node_modules/columnify/index.js
blob: 63257dceccacdd7f80e81a215c03ba21bfd30605 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
"use strict"

var wcwidth = require('./width')
var utils = require('./utils')
var padRight = utils.padRight
var padLeft = utils.padLeft
var splitIntoLines = utils.splitIntoLines
var splitLongWords = utils.splitLongWords
var truncateString = utils.truncateString

var DEFAULTS = {
  maxWidth: Infinity,
  minWidth: 0,
  columnSplitter: ' ',
  truncate: false,
  truncateMarker: '…',
  preserveNewLines: false,
  headingTransform: function(key) {
    return key.toUpperCase()
  },
  dataTransform: function(cell, column, index) {
    return cell
  }
}

module.exports = function(items, options) {

  options = options || {}

  var columnConfigs = options.config || {}
  delete options.config // remove config so doesn't appear on every column.

  var maxLineWidth = options.maxLineWidth || Infinity
  delete options.maxLineWidth // this is a line control option, don't pass it to column

  // Option defaults inheritance:
  // options.config[columnName] => options => DEFAULTS
  options = mixin(options, DEFAULTS)
  options.config = options.config || Object.create(null)

  options.spacing = options.spacing || '\n' // probably useless
  options.preserveNewLines = !!options.preserveNewLines
  options.columns = options.columns || options.include // alias include/columns, prefer columns if supplied
  var columnNames = options.columns || [] // optional user-supplied columns to include

  items = toArray(items, columnNames)

  // if not suppled column names, automatically determine columns from data keys
  if (!columnNames.length) {
    items.forEach(function(item) {
      for (var columnName in item) {
        if (columnNames.indexOf(columnName) === -1) columnNames.push(columnName)
      }
    })
  }

  // initialize column defaults (each column inherits from options.config)
  var columns = columnNames.reduce(function(columns, columnName) {
    var column = Object.create(options)
    columns[columnName] = mixin(column, columnConfigs[columnName])
    return columns
  }, Object.create(null))

  // sanitize column settings
  columnNames.forEach(function(columnName) {
    var column = columns[columnName]
    column.maxWidth = Math.ceil(column.maxWidth)
    column.minWidth = Math.ceil(column.minWidth)
    column.truncate = !!column.truncate
  })

  // sanitize data
  items = items.map(function(item) {
    var result = Object.create(null)
    columnNames.forEach(function(columnName) {
      // null/undefined -> ''
      result[columnName] = item[columnName] != null ? item[columnName] : ''
      // toString everything
      result[columnName] = '' + result[columnName]
      if (columns[columnName].preserveNewLines) {
        // merge non-newline whitespace chars
        result[columnName] = result[columnName].replace(/[^\S\n]/gmi, ' ')
      } else {
        // merge all whitespace chars
        result[columnName] = result[columnName].replace(/\s/gmi, ' ')
      }
    })
    return result
  })

  // transform data cells
  columnNames.forEach(function(columnName) {
    var column = columns[columnName]
    items = items.map(function(item, index) {
      item[columnName] = column.dataTransform(item[columnName], column, index)
      return item
    })
  })

  // add headers
  var headers = {}
  columnNames.forEach(function(columnName) {
    var column = columns[columnName]
    headers[columnName] = column.headingTransform(columnName)
  })
  items.unshift(headers)

  // get actual max-width between min & max
  // based on length of data in columns
  columnNames.forEach(function(columnName) {
    var column = columns[columnName]
    column.width = items.map(function(item) {
      return item[columnName]
    }).reduce(function(min, cur) {
      return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))))
    }, 0)
  })

  // split long words so they can break onto multiple lines
  columnNames.forEach(function(columnName) {
    var column = columns[columnName]
    items = items.map(function(item) {
      item[columnName] = splitLongWords(item[columnName], column.width, column.truncateMarker)
      return item
    })
  })

  // wrap long lines. each item is now an array of lines.
  columnNames.forEach(function(columnName) {
    var column = columns[columnName]
    items = items.map(function(item, index) {
      var cell = item[columnName]
      item[columnName] = splitIntoLines(cell, column.width)

      // if truncating required, only include first line + add truncation char
      if (column.truncate && item[columnName].length > 1) {
          item[columnName] = splitIntoLines(cell, column.width - wcwidth(column.truncateMarker))
          var firstLine = item[columnName][0]
          if (!endsWith(firstLine, column.truncateMarker)) item[columnName][0] += column.truncateMarker
          item[columnName] = item[columnName].slice(0, 1)
      }
      return item
    })
  })

  // recalculate column widths from truncated output/lines
  columnNames.forEach(function(columnName) {
    var column = columns[columnName]
    column.width = items.map(function(item) {
      return item[columnName].reduce(function(min, cur) {
        return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))))
      }, 0)
    }).reduce(function(min, cur) {
      return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur)))
    }, 0)
  })


  var rows = createRows(items, columns, columnNames) // merge lines into rows
  // conceive output
  return rows.reduce(function(output, row) {
    return output.concat(row.reduce(function(rowOut, line) {
      return rowOut.concat(line.join(options.columnSplitter))
    }, []))
  }, []).map(function(line) {
    return truncateString(line, maxLineWidth)
  }).join(options.spacing)
}

/**
 * Convert wrapped lines into rows with padded values.
 *
 * @param Array items data to process
 * @param Array columns column width settings for wrapping
 * @param Array columnNames column ordering
 * @return Array items wrapped in arrays, corresponding to lines
 */

function createRows(items, columns, columnNames) {
  return items.map(function(item) {
    var row = []
    var numLines = 0
    columnNames.forEach(function(columnName) {
      numLines = Math.max(numLines, item[columnName].length)
    })
    // combine matching lines of each rows
    for (var i = 0; i < numLines; i++) {
      row[i] = row[i] || []
      columnNames.forEach(function(columnName) {
        var column = columns[columnName]
        var val = item[columnName][i] || '' // || '' ensures empty columns get padded
        if (column.align == 'right') row[i].push(padLeft(val, column.width))
        else row[i].push(padRight(val, column.width))
      })
    }
    return row
  })
}

/**
 * Generic source->target mixin.
 * Copy properties from `source` into `target` if target doesn't have them.
 * Destructive. Modifies `target`.
 *
 * @param target Object target for mixin properties.
 * @param source Object source of mixin properties.
 * @return Object `target` after mixin applied.
 */

function mixin(target, source) {
  source = source || {}
  for (var key in source) {
    if (target.hasOwnProperty(key)) continue
    target[key] = source[key]
  }
  return target
}

/**
 * Adapted from String.prototype.endsWith polyfill.
 */

function endsWith(target, searchString, position) {
  position = position || target.length;
  position = position - searchString.length;
  var lastIndex = target.lastIndexOf(searchString);
  return lastIndex !== -1 && lastIndex === position;
}


function toArray(items, columnNames) {
  if (Array.isArray(items)) return items
  var rows = []
  for (var key in items) {
    var item = {}
    item[columnNames[0] || 'key'] = key
    item[columnNames[1] || 'value'] = items[key]
    rows.push(item)
  }
  return rows
}