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