index.js 7.5 KB

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