as-table.js 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. "use strict";
  2. const O = Object
  3. , { first, strlen } = require ('printable-characters') // handles ANSI codes and invisible characters
  4. , limit = (s, n) => (first (s, n - 1) + '…')
  5. const asColumns = (rows, cfg_) => {
  6. const
  7. zip = (arrs, f) => arrs.reduce ((a, b) => b.map ((b, i) => [...a[i] || [], b]), []).map (args => f (...args)),
  8. /* Convert cell data to string (converting multiline text to singleline) */
  9. cells = rows.map (r => r.map (c => c.replace (/\n/g, '\\n'))),
  10. /* Compute column widths (per row) and max widths (per column) */
  11. cellWidths = cells.map (r => r.map (strlen)),
  12. maxWidths = zip (cellWidths, Math.max),
  13. /* Default config */
  14. cfg = O.assign ({
  15. delimiter: ' ',
  16. minColumnWidths: maxWidths.map (x => 0),
  17. maxTotalWidth: 0 }, cfg_),
  18. delimiterLength = strlen (cfg.delimiter),
  19. /* Project desired column widths, taking maxTotalWidth and minColumnWidths in account. */
  20. totalWidth = maxWidths.reduce ((a, b) => a + b, 0),
  21. relativeWidths = maxWidths.map (w => w / totalWidth),
  22. maxTotalWidth = cfg.maxTotalWidth - (delimiterLength * (maxWidths.length - 1)),
  23. excessWidth = Math.max (0, totalWidth - maxTotalWidth),
  24. computedWidths = zip ([cfg.minColumnWidths, maxWidths, relativeWidths],
  25. (min, max, relative) => Math.max (min, Math.floor (max - excessWidth * relative))),
  26. /* This is how many symbols we should pad or cut (per column). */
  27. restCellWidths = cellWidths.map (widths => zip ([computedWidths, widths], (a, b) => a - b))
  28. /* Perform final composition. */
  29. return zip ([cells, restCellWidths], (a, b) =>
  30. zip ([a, b], (str, w) => (w >= 0)
  31. ? (cfg.right ? (' '.repeat (w) + str) : (str + ' '.repeat (w)))
  32. : (limit (str, strlen (str) + w))).join (cfg.delimiter))
  33. }
  34. const asTable = cfg => O.assign (arr => {
  35. /* Print arrays */
  36. if (arr[0] && Array.isArray (arr[0])) {
  37. return asColumns (arr.map (r => r.map (
  38. (c, i) => (c === undefined) ? '' : cfg.print (c, i)
  39. )
  40. ),
  41. cfg).join ('\n')
  42. }
  43. /* Print objects */
  44. const colNames = [...new Set ([].concat (...arr.map (O.keys)))],
  45. columns = [colNames.map (cfg.title),
  46. ...arr.map (o => colNames.map (
  47. key => (o[key] === undefined) ? '' : cfg.print (o[key], key)
  48. )
  49. )
  50. ],
  51. lines = asColumns (columns, cfg)
  52. return (cfg.dash ? [lines[0], cfg.dash.repeat (strlen (lines[0])), ...lines.slice (1)] : lines).join ('\n')
  53. }, cfg, {
  54. configure: newConfig => asTable (O.assign ({}, cfg, newConfig)),
  55. })
  56. module.exports = asTable ({
  57. maxTotalWidth: Number.MAX_SAFE_INTEGER,
  58. print: String,
  59. title: String,
  60. dash: '-',
  61. right: false
  62. })