columnify.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. "use strict";
  2. var wcwidth = require('./width');
  3. var _require = require('./utils');
  4. var padRight = _require.padRight;
  5. var padCenter = _require.padCenter;
  6. var padLeft = _require.padLeft;
  7. var splitIntoLines = _require.splitIntoLines;
  8. var splitLongWords = _require.splitLongWords;
  9. var truncateString = _require.truncateString;
  10. var DEFAULT_HEADING_TRANSFORM = function DEFAULT_HEADING_TRANSFORM(key) {
  11. return key.toUpperCase();
  12. };
  13. var DEFAULT_DATA_TRANSFORM = function DEFAULT_DATA_TRANSFORM(cell, column, index) {
  14. return cell;
  15. };
  16. var DEFAULTS = Object.freeze({
  17. maxWidth: Infinity,
  18. minWidth: 0,
  19. columnSplitter: ' ',
  20. truncate: false,
  21. truncateMarker: '…',
  22. preserveNewLines: false,
  23. paddingChr: ' ',
  24. showHeaders: true,
  25. headingTransform: DEFAULT_HEADING_TRANSFORM,
  26. dataTransform: DEFAULT_DATA_TRANSFORM
  27. });
  28. module.exports = function (items) {
  29. var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
  30. var columnConfigs = options.config || {};
  31. delete options.config; // remove config so doesn't appear on every column.
  32. var maxLineWidth = options.maxLineWidth || Infinity;
  33. if (maxLineWidth === 'auto') maxLineWidth = process.stdout.columns || Infinity;
  34. delete options.maxLineWidth; // this is a line control option, don't pass it to column
  35. // Option defaults inheritance:
  36. // options.config[columnName] => options => DEFAULTS
  37. options = mixin({}, DEFAULTS, options);
  38. options.config = options.config || Object.create(null);
  39. options.spacing = options.spacing || '\n'; // probably useless
  40. options.preserveNewLines = !!options.preserveNewLines;
  41. options.showHeaders = !!options.showHeaders;
  42. options.columns = options.columns || options.include; // alias include/columns, prefer columns if supplied
  43. var columnNames = options.columns || []; // optional user-supplied columns to include
  44. items = toArray(items, columnNames);
  45. // if not suppled column names, automatically determine columns from data keys
  46. if (!columnNames.length) {
  47. items.forEach(function (item) {
  48. for (var columnName in item) {
  49. if (columnNames.indexOf(columnName) === -1) columnNames.push(columnName);
  50. }
  51. });
  52. }
  53. // initialize column defaults (each column inherits from options.config)
  54. var columns = columnNames.reduce(function (columns, columnName) {
  55. var column = Object.create(options);
  56. columns[columnName] = mixin(column, columnConfigs[columnName]);
  57. return columns;
  58. }, Object.create(null));
  59. // sanitize column settings
  60. columnNames.forEach(function (columnName) {
  61. var column = columns[columnName];
  62. column.name = columnName;
  63. column.maxWidth = Math.ceil(column.maxWidth);
  64. column.minWidth = Math.ceil(column.minWidth);
  65. column.truncate = !!column.truncate;
  66. column.align = column.align || 'left';
  67. });
  68. // sanitize data
  69. items = items.map(function (item) {
  70. var result = Object.create(null);
  71. columnNames.forEach(function (columnName) {
  72. // null/undefined -> ''
  73. result[columnName] = item[columnName] != null ? item[columnName] : '';
  74. // toString everything
  75. result[columnName] = '' + result[columnName];
  76. if (columns[columnName].preserveNewLines) {
  77. // merge non-newline whitespace chars
  78. result[columnName] = result[columnName].replace(/[^\S\n]/gmi, ' ');
  79. } else {
  80. // merge all whitespace chars
  81. result[columnName] = result[columnName].replace(/\s/gmi, ' ');
  82. }
  83. });
  84. return result;
  85. });
  86. // transform data cells
  87. columnNames.forEach(function (columnName) {
  88. var column = columns[columnName];
  89. items = items.map(function (item, index) {
  90. var col = Object.create(column);
  91. item[columnName] = column.dataTransform(item[columnName], col, index);
  92. var changedKeys = Object.keys(col);
  93. // disable default heading transform if we wrote to column.name
  94. if (changedKeys.indexOf('name') !== -1) {
  95. if (column.headingTransform !== DEFAULT_HEADING_TRANSFORM) return;
  96. column.headingTransform = function (heading) {
  97. return heading;
  98. };
  99. }
  100. changedKeys.forEach(function (key) {
  101. return column[key] = col[key];
  102. });
  103. return item;
  104. });
  105. });
  106. // add headers
  107. var headers = {};
  108. if (options.showHeaders) {
  109. columnNames.forEach(function (columnName) {
  110. var column = columns[columnName];
  111. if (!column.showHeaders) {
  112. headers[columnName] = '';
  113. return;
  114. }
  115. headers[columnName] = column.headingTransform(column.name);
  116. });
  117. items.unshift(headers);
  118. }
  119. // get actual max-width between min & max
  120. // based on length of data in columns
  121. columnNames.forEach(function (columnName) {
  122. var column = columns[columnName];
  123. column.width = items.map(function (item) {
  124. return item[columnName];
  125. }).reduce(function (min, cur) {
  126. // if already at maxWidth don't bother testing
  127. if (min >= column.maxWidth) return min;
  128. return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))));
  129. }, 0);
  130. });
  131. // split long words so they can break onto multiple lines
  132. columnNames.forEach(function (columnName) {
  133. var column = columns[columnName];
  134. items = items.map(function (item) {
  135. item[columnName] = splitLongWords(item[columnName], column.width, column.truncateMarker);
  136. return item;
  137. });
  138. });
  139. // wrap long lines. each item is now an array of lines.
  140. columnNames.forEach(function (columnName) {
  141. var column = columns[columnName];
  142. items = items.map(function (item, index) {
  143. var cell = item[columnName];
  144. item[columnName] = splitIntoLines(cell, column.width);
  145. // if truncating required, only include first line + add truncation char
  146. if (column.truncate && item[columnName].length > 1) {
  147. item[columnName] = splitIntoLines(cell, column.width - wcwidth(column.truncateMarker));
  148. var firstLine = item[columnName][0];
  149. if (!endsWith(firstLine, column.truncateMarker)) item[columnName][0] += column.truncateMarker;
  150. item[columnName] = item[columnName].slice(0, 1);
  151. }
  152. return item;
  153. });
  154. });
  155. // recalculate column widths from truncated output/lines
  156. columnNames.forEach(function (columnName) {
  157. var column = columns[columnName];
  158. column.width = items.map(function (item) {
  159. return item[columnName].reduce(function (min, cur) {
  160. if (min >= column.maxWidth) return min;
  161. return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))));
  162. }, 0);
  163. }).reduce(function (min, cur) {
  164. if (min >= column.maxWidth) return min;
  165. return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur)));
  166. }, 0);
  167. });
  168. var rows = createRows(items, columns, columnNames, options.paddingChr); // merge lines into rows
  169. // conceive output
  170. return rows.reduce(function (output, row) {
  171. return output.concat(row.reduce(function (rowOut, line) {
  172. return rowOut.concat(line.join(options.columnSplitter));
  173. }, []));
  174. }, []).map(function (line) {
  175. return truncateString(line, maxLineWidth);
  176. }).join(options.spacing);
  177. };
  178. /**
  179. * Convert wrapped lines into rows with padded values.
  180. *
  181. * @param Array items data to process
  182. * @param Array columns column width settings for wrapping
  183. * @param Array columnNames column ordering
  184. * @return Array items wrapped in arrays, corresponding to lines
  185. */
  186. function createRows(items, columns, columnNames, paddingChr) {
  187. return items.map(function (item) {
  188. var row = [];
  189. var numLines = 0;
  190. columnNames.forEach(function (columnName) {
  191. numLines = Math.max(numLines, item[columnName].length);
  192. });
  193. // combine matching lines of each rows
  194. var _loop = function _loop(i) {
  195. row[i] = row[i] || [];
  196. columnNames.forEach(function (columnName) {
  197. var column = columns[columnName];
  198. var val = item[columnName][i] || ''; // || '' ensures empty columns get padded
  199. if (column.align === 'right') row[i].push(padLeft(val, column.width, paddingChr));else if (column.align === 'center' || column.align === 'centre') row[i].push(padCenter(val, column.width, paddingChr));else row[i].push(padRight(val, column.width, paddingChr));
  200. });
  201. };
  202. for (var i = 0; i < numLines; i++) {
  203. _loop(i);
  204. }
  205. return row;
  206. });
  207. }
  208. /**
  209. * Object.assign
  210. *
  211. * @return Object Object with properties mixed in.
  212. */
  213. function mixin() {
  214. var _Object;
  215. if (Object.assign) return (_Object = Object).assign.apply(_Object, arguments);
  216. return ObjectAssign.apply(undefined, arguments);
  217. }
  218. function ObjectAssign(target, firstSource) {
  219. "use strict";
  220. if (target === undefined || target === null) throw new TypeError("Cannot convert first argument to object");
  221. var to = Object(target);
  222. var hasPendingException = false;
  223. var pendingException;
  224. for (var i = 1; i < arguments.length; i++) {
  225. var nextSource = arguments[i];
  226. if (nextSource === undefined || nextSource === null) continue;
  227. var keysArray = Object.keys(Object(nextSource));
  228. for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
  229. var nextKey = keysArray[nextIndex];
  230. try {
  231. var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
  232. if (desc !== undefined && desc.enumerable) to[nextKey] = nextSource[nextKey];
  233. } catch (e) {
  234. if (!hasPendingException) {
  235. hasPendingException = true;
  236. pendingException = e;
  237. }
  238. }
  239. }
  240. if (hasPendingException) throw pendingException;
  241. }
  242. return to;
  243. }
  244. /**
  245. * Adapted from String.prototype.endsWith polyfill.
  246. */
  247. function endsWith(target, searchString, position) {
  248. position = position || target.length;
  249. position = position - searchString.length;
  250. var lastIndex = target.lastIndexOf(searchString);
  251. return lastIndex !== -1 && lastIndex === position;
  252. }
  253. function toArray(items, columnNames) {
  254. if (Array.isArray(items)) return items;
  255. var rows = [];
  256. for (var key in items) {
  257. var item = {};
  258. item[columnNames[0] || 'key'] = key;
  259. item[columnNames[1] || 'value'] = items[key];
  260. rows.push(item);
  261. }
  262. return rows;
  263. }