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