index.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. /**
  2. * Module dependencies.
  3. */
  4. var colors = require('chalk')
  5. var utils = require('./utils')
  6. var repeat = utils.repeat
  7. var truncate = utils.truncate
  8. /**
  9. * Table constructor
  10. *
  11. * @param {Object} options
  12. * @api public
  13. */
  14. function Table (options) {
  15. this.options = utils.options({
  16. chars: {
  17. 'top': '─',
  18. 'top-mid': '┬',
  19. 'top-left': '┌',
  20. 'top-right': '┐',
  21. 'bottom': '─',
  22. 'bottom-mid': '┴',
  23. 'bottom-left': '└',
  24. 'bottom-right': '┘',
  25. 'left': '│',
  26. 'left-mid': '├',
  27. 'mid': '─',
  28. 'mid-mid': '┼',
  29. 'right': '│',
  30. 'right-mid': '┤',
  31. 'middle': '│'
  32. },
  33. truncate: '…',
  34. colWidths: [],
  35. colAligns: [],
  36. style: {
  37. 'padding-left': 1,
  38. 'padding-right': 1,
  39. head: ['red'],
  40. border: ['grey'],
  41. compact: false
  42. },
  43. head: []
  44. }, options)
  45. if (options.borders == false) {
  46. this.options.chars = {
  47. 'top': '',
  48. 'top-mid': '',
  49. 'top-left': '',
  50. 'top-right': '',
  51. 'bottom': '',
  52. 'bottom-mid': '',
  53. 'bottom-left': '',
  54. 'bottom-right': '',
  55. 'left': '',
  56. 'left-mid': '',
  57. 'mid': '',
  58. 'mid-mid': '',
  59. 'right': '',
  60. 'right-mid': '',
  61. 'middle': ''
  62. }
  63. }
  64. };
  65. /**
  66. * Inherit from Array.
  67. */
  68. Table.prototype = new Array
  69. /**
  70. * Width getter
  71. *
  72. * @return {Number} width
  73. * @api public
  74. */
  75. Table.prototype.__defineGetter__('width', function () {
  76. var str = this.toString().split('\n')
  77. if (str.length) return str[0].length
  78. return 0
  79. })
  80. /**
  81. * Render to a string.
  82. *
  83. * @return {String} table representation
  84. * @api public
  85. */
  86. Table.prototype.render =
  87. Table.prototype.toString = function () {
  88. var ret = ''
  89. var options = this.options
  90. var style = options.style
  91. var head = options.head
  92. var chars = options.chars
  93. var truncater = options.truncate
  94. var colWidths = options.colWidths || new Array(this.head.length)
  95. var totalWidth = 0
  96. if (!head.length && !this.length) return ''
  97. if (!colWidths.length) {
  98. var everyRows = this.slice(0)
  99. if (head.length) { everyRows = everyRows.concat([head]) };
  100. everyRows.forEach(function (cells) {
  101. // horizontal (arrays)
  102. if (Array.isArray(cells) && cells.length) {
  103. extractColumnWidths(cells)
  104. // vertical (objects)
  105. } else {
  106. var headerCell = Object.keys(cells)[0]
  107. var valueCell = cells[headerCell]
  108. colWidths[0] = Math.max(colWidths[0] || 0, getWidth(headerCell) || 0)
  109. // cross (objects w/ array values)
  110. if (Array.isArray(valueCell) && valueCell.length) {
  111. extractColumnWidths(valueCell, 1)
  112. } else {
  113. colWidths[1] = Math.max(colWidths[1] || 0, getWidth(valueCell) || 0)
  114. }
  115. }
  116. })
  117. };
  118. totalWidth = (colWidths.length === 1 ? colWidths[0] : colWidths.reduce(
  119. function (a, b) {
  120. return a + b
  121. })) + colWidths.length + 1
  122. function extractColumnWidths (arr, offset) {
  123. offset = offset || 0
  124. arr.forEach(function (cell, i) {
  125. colWidths[i + offset] = Math.max(colWidths[i + offset] || 0, getWidth(cell) || 0)
  126. })
  127. };
  128. function getWidth (obj) {
  129. return typeof obj === 'object' && obj && obj.width !== undefined
  130. ? obj.width
  131. : ((typeof obj === 'object' && obj !== null ? utils.strlen(obj.text) : utils.strlen(obj)) + (style['padding-left'] || 0) + (style['padding-right'] || 0))
  132. }
  133. // draws a line
  134. function line (line, left, right, intersection) {
  135. var width = 0
  136. line = left + repeat(line, totalWidth - 2) + right
  137. colWidths.forEach(function (w, i) {
  138. if (i === colWidths.length - 1) return
  139. width += w + 1
  140. line = line.substr(0, width) + intersection + line.substr(width + 1)
  141. })
  142. return applyStyles(options.style.border, line)
  143. };
  144. // draws the top line
  145. function lineTop () {
  146. var l = line(chars.top,
  147. chars['top-left'] || chars.top,
  148. chars['top-right'] || chars.top,
  149. chars['top-mid'])
  150. if (l) {
  151. ret += l + '\n'
  152. }
  153. };
  154. function generateRow (items, style) {
  155. var cells = []
  156. var maxHeight = 0
  157. // prepare vertical and cross table data
  158. if (!Array.isArray(items) && typeof items === 'object') {
  159. var key = Object.keys(items)[0]
  160. var value = items[key]
  161. var firstCellHead = true
  162. if (Array.isArray(value)) {
  163. items = value
  164. items.unshift(key)
  165. } else {
  166. items = [key, value]
  167. }
  168. }
  169. // transform array of item strings into structure of cells
  170. items.forEach(function (item, i) {
  171. var contents = (item === null || item === undefined ? '' : item).toString().split('\n').reduce(function (memo, l) {
  172. memo.push(string(l, i))
  173. return memo
  174. }, [])
  175. var height = contents.length
  176. if (height > maxHeight) { maxHeight = height };
  177. cells.push({ contents: contents, height: height })
  178. })
  179. // transform vertical cells into horizontal lines
  180. var lines = new Array(maxHeight)
  181. cells.forEach(function (cell, i) {
  182. cell.contents.forEach(function (line, j) {
  183. if (!lines[j]) { lines[j] = [] };
  184. if (style || (firstCellHead && i === 0 && options.style.head)) {
  185. line = applyStyles(options.style.head, line)
  186. }
  187. lines[j].push(line)
  188. })
  189. // populate empty lines in cell
  190. for (var j = cell.height, l = maxHeight; j < l; j++) {
  191. if (!lines[j]) { lines[j] = [] };
  192. lines[j].push(string('', i))
  193. }
  194. })
  195. var ret = ''
  196. lines.forEach(function (line, index) {
  197. if (ret.length > 0) {
  198. ret += '\n' + applyStyles(options.style.border, chars.left)
  199. }
  200. ret += line.join(applyStyles(options.style.border, chars.middle)) + applyStyles(options.style.border, chars.right)
  201. })
  202. return applyStyles(options.style.border, chars.left) + ret
  203. };
  204. function applyStyles (styles, subject) {
  205. if (!subject) {
  206. return ''
  207. }
  208. styles.forEach(function (style) {
  209. subject = colors[style](subject)
  210. })
  211. return subject
  212. };
  213. // renders a string, by padding it or truncating it
  214. function string (str, index) {
  215. str = String(typeof str === 'object' && str.text ? str.text : str)
  216. var length = utils.strlen(str)
  217. var width = colWidths[index] - (style['padding-left'] || 0) - (style['padding-right'] || 0)
  218. var align = options.colAligns[index] || 'left'
  219. return repeat(' ', style['padding-left'] || 0) +
  220. (length === width ? str
  221. : (length < width
  222. ? str.padEnd((width + (str.length - length)), ' ', align === 'left' ? 'right'
  223. : (align === 'middle' ? 'both' : 'left'))
  224. : (truncater ? truncate(str, width, truncater) : str))
  225. ) +
  226. repeat(' ', style['padding-right'] || 0)
  227. };
  228. if (head.length) {
  229. lineTop()
  230. ret += generateRow(head, style.head) + '\n'
  231. }
  232. if (this.length) {
  233. this.forEach(function (cells, i) {
  234. if (!head.length && i === 0) { lineTop() } else {
  235. if (!style.compact || i < (!!head.length) ? 1 : 0 || cells.length === 0) {
  236. var l = line(chars.mid
  237. , chars['left-mid']
  238. , chars['right-mid']
  239. , chars['mid-mid'])
  240. if (l) { ret += l + '\n' }
  241. }
  242. }
  243. if (Array.isArray(cells) && !cells.length) {
  244. return
  245. } else {
  246. ret += generateRow(cells) + '\n'
  247. };
  248. })
  249. }
  250. var l = line(chars.bottom,
  251. chars['bottom-left'] || chars.bottom,
  252. chars['bottom-right'] || chars.bottom,
  253. chars['bottom-mid'])
  254. if (l) {
  255. ret += l
  256. } else {
  257. // trim the last '\n' if we didn't add the bottom decoration
  258. ret = ret.slice(0, -1)
  259. }
  260. return ret
  261. }
  262. /**
  263. * Module exports.
  264. */
  265. module.exports = Table
  266. module.exports.version = '2.0.0'