index.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. var wrap = require('wordwrap'),
  2. align = {
  3. right: require('right-align'),
  4. center: require('center-align')
  5. },
  6. top = 0,
  7. right = 1,
  8. bottom = 2,
  9. left = 3
  10. function UI (opts) {
  11. this.width = opts.width
  12. this.wrap = opts.wrap
  13. this.rows = []
  14. }
  15. UI.prototype.span = function () {
  16. var cols = this.div.apply(this, arguments)
  17. cols.span = true
  18. }
  19. UI.prototype.div = function () {
  20. if (arguments.length === 0) this.div('')
  21. if (this.wrap && this._shouldApplyLayoutDSL.apply(this, arguments)) {
  22. return this._applyLayoutDSL(arguments[0])
  23. }
  24. var cols = []
  25. for (var i = 0, arg; (arg = arguments[i]) !== undefined; i++) {
  26. if (typeof arg === 'string') cols.push(this._colFromString(arg))
  27. else cols.push(arg)
  28. }
  29. this.rows.push(cols)
  30. return cols
  31. }
  32. UI.prototype._shouldApplyLayoutDSL = function () {
  33. return arguments.length === 1 && typeof arguments[0] === 'string' &&
  34. /[\t\n]/.test(arguments[0])
  35. }
  36. UI.prototype._applyLayoutDSL = function (str) {
  37. var _this = this,
  38. rows = str.split('\n'),
  39. leftColumnWidth = 0
  40. // simple heuristic for layout, make sure the
  41. // second column lines up along the left-hand.
  42. // don't allow the first column to take up more
  43. // than 50% of the screen.
  44. rows.forEach(function (row) {
  45. var columns = row.split('\t')
  46. if (columns.length > 1 && columns[0].length > leftColumnWidth) {
  47. leftColumnWidth = Math.min(
  48. Math.floor(_this.width * 0.5),
  49. columns[0].length
  50. )
  51. }
  52. })
  53. // generate a table:
  54. // replacing ' ' with padding calculations.
  55. // using the algorithmically generated width.
  56. rows.forEach(function (row) {
  57. var columns = row.split('\t')
  58. _this.div.apply(_this, columns.map(function (r, i) {
  59. return {
  60. text: r.trim(),
  61. padding: [0, r.match(/\s*$/)[0].length, 0, r.match(/^\s*/)[0].length],
  62. width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
  63. }
  64. }))
  65. })
  66. return this.rows[this.rows.length - 1]
  67. }
  68. UI.prototype._colFromString = function (str) {
  69. return {
  70. text: str
  71. }
  72. }
  73. UI.prototype.toString = function () {
  74. var _this = this,
  75. lines = []
  76. _this.rows.forEach(function (row, i) {
  77. _this.rowToString(row, lines)
  78. })
  79. // don't display any lines with the
  80. // hidden flag set.
  81. lines = lines.filter(function (line) {
  82. return !line.hidden
  83. })
  84. return lines.map(function (line) {
  85. return line.text
  86. }).join('\n')
  87. }
  88. UI.prototype.rowToString = function (row, lines) {
  89. var _this = this,
  90. paddingLeft,
  91. rrows = this._rasterize(row),
  92. str = '',
  93. ts,
  94. width,
  95. wrapWidth
  96. rrows.forEach(function (rrow, r) {
  97. str = ''
  98. rrow.forEach(function (col, c) {
  99. ts = '' // temporary string used during alignment/padding.
  100. width = row[c].width // the width with padding.
  101. wrapWidth = _this._negatePadding(row[c]) // the width without padding.
  102. for (var i = 0; i < Math.max(wrapWidth, col.length); i++) {
  103. ts += col.charAt(i) || ' '
  104. }
  105. // align the string within its column.
  106. if (row[c].align && row[c].align !== 'left' && _this.wrap) {
  107. ts = align[row[c].align](ts.trim() + '\n' + new Array(wrapWidth + 1).join(' '))
  108. .split('\n')[0]
  109. if (ts.length < wrapWidth) ts += new Array(width - ts.length).join(' ')
  110. }
  111. // add left/right padding and print string.
  112. paddingLeft = (row[c].padding || [0, 0, 0, 0])[left]
  113. if (paddingLeft) str += new Array(row[c].padding[left] + 1).join(' ')
  114. str += ts
  115. if (row[c].padding && row[c].padding[right]) str += new Array(row[c].padding[right] + 1).join(' ')
  116. // if prior row is span, try to render the
  117. // current row on the prior line.
  118. if (r === 0 && lines.length > 0) {
  119. str = _this._renderInline(str, lines[lines.length - 1], paddingLeft)
  120. }
  121. })
  122. // remove trailing whitespace.
  123. lines.push({
  124. text: str.replace(/ +$/, ''),
  125. span: row.span
  126. })
  127. })
  128. return lines
  129. }
  130. // if the full 'source' can render in
  131. // the target line, do so.
  132. UI.prototype._renderInline = function (source, previousLine, paddingLeft) {
  133. var target = previousLine.text,
  134. str = ''
  135. if (!previousLine.span) return source
  136. // if we're not applying wrapping logic,
  137. // just always append to the span.
  138. if (!this.wrap) {
  139. previousLine.hidden = true
  140. return target + source
  141. }
  142. for (var i = 0, tc, sc; i < Math.max(source.length, target.length); i++) {
  143. tc = target.charAt(i) || ' '
  144. sc = source.charAt(i) || ' '
  145. // we tried to overwrite a character in the other string.
  146. if (tc !== ' ' && sc !== ' ') return source
  147. // there is not enough whitespace to maintain padding.
  148. if (sc !== ' ' && i < paddingLeft + target.length) return source
  149. // :thumbsup:
  150. if (tc === ' ') str += sc
  151. else str += tc
  152. }
  153. previousLine.hidden = true
  154. return str
  155. }
  156. UI.prototype._rasterize = function (row) {
  157. var _this = this,
  158. i,
  159. rrow,
  160. rrows = [],
  161. widths = this._columnWidths(row),
  162. wrapped
  163. // word wrap all columns, and create
  164. // a data-structure that is easy to rasterize.
  165. row.forEach(function (col, c) {
  166. // leave room for left and right padding.
  167. col.width = widths[c]
  168. if (_this.wrap) wrapped = wrap.hard(_this._negatePadding(col))(col.text).split('\n')
  169. else wrapped = col.text.split('\n')
  170. // add top and bottom padding.
  171. if (col.padding) {
  172. for (i = 0; i < (col.padding[top] || 0); i++) wrapped.unshift('')
  173. for (i = 0; i < (col.padding[bottom] || 0); i++) wrapped.push('')
  174. }
  175. wrapped.forEach(function (str, r) {
  176. if (!rrows[r]) rrows.push([])
  177. rrow = rrows[r]
  178. for (var i = 0; i < c; i++) {
  179. if (rrow[i] === undefined) rrow.push('')
  180. }
  181. rrow.push(str)
  182. })
  183. })
  184. return rrows
  185. }
  186. UI.prototype._negatePadding = function (col) {
  187. var wrapWidth = col.width
  188. if (col.padding) wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0)
  189. return wrapWidth
  190. }
  191. UI.prototype._columnWidths = function (row) {
  192. var _this = this,
  193. widths = [],
  194. unset = row.length,
  195. unsetWidth,
  196. remainingWidth = this.width
  197. // column widths can be set in config.
  198. row.forEach(function (col, i) {
  199. if (col.width) {
  200. unset--
  201. widths[i] = col.width
  202. remainingWidth -= col.width
  203. } else {
  204. widths[i] = undefined
  205. }
  206. })
  207. // any unset widths should be calculated.
  208. if (unset) unsetWidth = Math.floor(remainingWidth / unset)
  209. widths.forEach(function (w, i) {
  210. if (!_this.wrap) widths[i] = row[i].width || row[i].text.length
  211. else if (w === undefined) widths[i] = Math.max(unsetWidth, _minWidth(row[i]))
  212. })
  213. return widths
  214. }
  215. // calculates the minimum width of
  216. // a column, based on padding preferences.
  217. function _minWidth (col) {
  218. var padding = col.padding || []
  219. return 1 + (padding[left] || 0) + (padding[right] || 0)
  220. }
  221. module.exports = function (opts) {
  222. opts = opts || {}
  223. return new UI({
  224. width: (opts || {}).width || 80,
  225. wrap: typeof opts.wrap === 'boolean' ? opts.wrap : true
  226. })
  227. }