274 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			274 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| var wrap = require('wordwrap'),
 | |
|   align = {
 | |
|     right: require('right-align'),
 | |
|     center: require('center-align')
 | |
|   },
 | |
|   top = 0,
 | |
|   right = 1,
 | |
|   bottom = 2,
 | |
|   left = 3
 | |
| 
 | |
| function UI (opts) {
 | |
|   this.width = opts.width
 | |
|   this.wrap = opts.wrap
 | |
|   this.rows = []
 | |
| }
 | |
| 
 | |
| UI.prototype.span = function () {
 | |
|   var cols = this.div.apply(this, arguments)
 | |
|   cols.span = true
 | |
| }
 | |
| 
 | |
| UI.prototype.div = function () {
 | |
|   if (arguments.length === 0) this.div('')
 | |
|   if (this.wrap && this._shouldApplyLayoutDSL.apply(this, arguments)) {
 | |
|     return this._applyLayoutDSL(arguments[0])
 | |
|   }
 | |
| 
 | |
|   var cols = []
 | |
| 
 | |
|   for (var i = 0, arg; (arg = arguments[i]) !== undefined; i++) {
 | |
|     if (typeof arg === 'string') cols.push(this._colFromString(arg))
 | |
|     else cols.push(arg)
 | |
|   }
 | |
| 
 | |
|   this.rows.push(cols)
 | |
|   return cols
 | |
| }
 | |
| 
 | |
| UI.prototype._shouldApplyLayoutDSL = function () {
 | |
|   return arguments.length === 1 && typeof arguments[0] === 'string' &&
 | |
|     /[\t\n]/.test(arguments[0])
 | |
| }
 | |
| 
 | |
| UI.prototype._applyLayoutDSL = function (str) {
 | |
|   var _this = this,
 | |
|     rows = str.split('\n'),
 | |
|     leftColumnWidth = 0
 | |
| 
 | |
|   // simple heuristic for layout, make sure the
 | |
|   // second column lines up along the left-hand.
 | |
|   // don't allow the first column to take up more
 | |
|   // than 50% of the screen.
 | |
|   rows.forEach(function (row) {
 | |
|     var columns = row.split('\t')
 | |
|     if (columns.length > 1 && columns[0].length > leftColumnWidth) {
 | |
|       leftColumnWidth = Math.min(
 | |
|         Math.floor(_this.width * 0.5),
 | |
|         columns[0].length
 | |
|       )
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   // generate a table:
 | |
|   //  replacing ' ' with padding calculations.
 | |
|   //  using the algorithmically generated width.
 | |
|   rows.forEach(function (row) {
 | |
|     var columns = row.split('\t')
 | |
|     _this.div.apply(_this, columns.map(function (r, i) {
 | |
|       return {
 | |
|         text: r.trim(),
 | |
|         padding: [0, r.match(/\s*$/)[0].length, 0, r.match(/^\s*/)[0].length],
 | |
|         width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
 | |
|       }
 | |
|     }))
 | |
|   })
 | |
| 
 | |
|   return this.rows[this.rows.length - 1]
 | |
| }
 | |
| 
 | |
| UI.prototype._colFromString = function (str) {
 | |
|   return {
 | |
|     text: str
 | |
|   }
 | |
| }
 | |
| 
 | |
| UI.prototype.toString = function () {
 | |
|   var _this = this,
 | |
|     lines = []
 | |
| 
 | |
|   _this.rows.forEach(function (row, i) {
 | |
|     _this.rowToString(row, lines)
 | |
|   })
 | |
| 
 | |
|   // don't display any lines with the
 | |
|   // hidden flag set.
 | |
|   lines = lines.filter(function (line) {
 | |
|     return !line.hidden
 | |
|   })
 | |
| 
 | |
|   return lines.map(function (line) {
 | |
|     return line.text
 | |
|   }).join('\n')
 | |
| }
 | |
| 
 | |
| UI.prototype.rowToString = function (row, lines) {
 | |
|   var _this = this,
 | |
|     paddingLeft,
 | |
|     rrows = this._rasterize(row),
 | |
|     str = '',
 | |
|     ts,
 | |
|     width,
 | |
|     wrapWidth
 | |
| 
 | |
|   rrows.forEach(function (rrow, r) {
 | |
|     str = ''
 | |
|     rrow.forEach(function (col, c) {
 | |
|       ts = '' // temporary string used during alignment/padding.
 | |
|       width = row[c].width // the width with padding.
 | |
|       wrapWidth = _this._negatePadding(row[c]) // the width without padding.
 | |
| 
 | |
|       for (var i = 0; i < Math.max(wrapWidth, col.length); i++) {
 | |
|         ts += col.charAt(i) || ' '
 | |
|       }
 | |
| 
 | |
|       // align the string within its column.
 | |
|       if (row[c].align && row[c].align !== 'left' && _this.wrap) {
 | |
|         ts = align[row[c].align](ts.trim() + '\n' + new Array(wrapWidth + 1).join(' '))
 | |
|           .split('\n')[0]
 | |
|         if (ts.length < wrapWidth) ts += new Array(width - ts.length).join(' ')
 | |
|       }
 | |
| 
 | |
|       // add left/right padding and print string.
 | |
|       paddingLeft = (row[c].padding || [0, 0, 0, 0])[left]
 | |
|       if (paddingLeft) str += new Array(row[c].padding[left] + 1).join(' ')
 | |
|       str += ts
 | |
|       if (row[c].padding && row[c].padding[right]) str += new Array(row[c].padding[right] + 1).join(' ')
 | |
| 
 | |
|       // if prior row is span, try to render the
 | |
|       // current row on the prior line.
 | |
|       if (r === 0 && lines.length > 0) {
 | |
|         str = _this._renderInline(str, lines[lines.length - 1], paddingLeft)
 | |
|       }
 | |
|     })
 | |
| 
 | |
|     // remove trailing whitespace.
 | |
|     lines.push({
 | |
|       text: str.replace(/ +$/, ''),
 | |
|       span: row.span
 | |
|     })
 | |
|   })
 | |
| 
 | |
|   return lines
 | |
| }
 | |
| 
 | |
| // if the full 'source' can render in
 | |
| // the target line, do so.
 | |
| UI.prototype._renderInline = function (source, previousLine, paddingLeft) {
 | |
|   var target = previousLine.text,
 | |
|     str = ''
 | |
| 
 | |
|   if (!previousLine.span) return source
 | |
| 
 | |
|   // if we're not applying wrapping logic,
 | |
|   // just always append to the span.
 | |
|   if (!this.wrap) {
 | |
|     previousLine.hidden = true
 | |
|     return target + source
 | |
|   }
 | |
| 
 | |
|   for (var i = 0, tc, sc; i < Math.max(source.length, target.length); i++) {
 | |
|     tc = target.charAt(i) || ' '
 | |
|     sc = source.charAt(i) || ' '
 | |
|     // we tried to overwrite a character in the other string.
 | |
|     if (tc !== ' ' && sc !== ' ') return source
 | |
|     // there is not enough whitespace to maintain padding.
 | |
|     if (sc !== ' ' && i < paddingLeft + target.length) return source
 | |
|     // :thumbsup:
 | |
|     if (tc === ' ') str += sc
 | |
|     else str += tc
 | |
|   }
 | |
| 
 | |
|   previousLine.hidden = true
 | |
| 
 | |
|   return str
 | |
| }
 | |
| 
 | |
| UI.prototype._rasterize = function (row) {
 | |
|   var _this = this,
 | |
|     i,
 | |
|     rrow,
 | |
|     rrows = [],
 | |
|     widths = this._columnWidths(row),
 | |
|     wrapped
 | |
| 
 | |
|   // word wrap all columns, and create
 | |
|   // a data-structure that is easy to rasterize.
 | |
|   row.forEach(function (col, c) {
 | |
|     // leave room for left and right padding.
 | |
|     col.width = widths[c]
 | |
|     if (_this.wrap) wrapped = wrap.hard(_this._negatePadding(col))(col.text).split('\n')
 | |
|     else wrapped = col.text.split('\n')
 | |
| 
 | |
|     // add top and bottom padding.
 | |
|     if (col.padding) {
 | |
|       for (i = 0; i < (col.padding[top] || 0); i++) wrapped.unshift('')
 | |
|       for (i = 0; i < (col.padding[bottom] || 0); i++) wrapped.push('')
 | |
|     }
 | |
| 
 | |
|     wrapped.forEach(function (str, r) {
 | |
|       if (!rrows[r]) rrows.push([])
 | |
| 
 | |
|       rrow = rrows[r]
 | |
| 
 | |
|       for (var i = 0; i < c; i++) {
 | |
|         if (rrow[i] === undefined) rrow.push('')
 | |
|       }
 | |
|       rrow.push(str)
 | |
|     })
 | |
|   })
 | |
| 
 | |
|   return rrows
 | |
| }
 | |
| 
 | |
| UI.prototype._negatePadding = function (col) {
 | |
|   var wrapWidth = col.width
 | |
|   if (col.padding) wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0)
 | |
|   return wrapWidth
 | |
| }
 | |
| 
 | |
| UI.prototype._columnWidths = function (row) {
 | |
|   var _this = this,
 | |
|     widths = [],
 | |
|     unset = row.length,
 | |
|     unsetWidth,
 | |
|     remainingWidth = this.width
 | |
| 
 | |
|   // column widths can be set in config.
 | |
|   row.forEach(function (col, i) {
 | |
|     if (col.width) {
 | |
|       unset--
 | |
|       widths[i] = col.width
 | |
|       remainingWidth -= col.width
 | |
|     } else {
 | |
|       widths[i] = undefined
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   // any unset widths should be calculated.
 | |
|   if (unset) unsetWidth = Math.floor(remainingWidth / unset)
 | |
|   widths.forEach(function (w, i) {
 | |
|     if (!_this.wrap) widths[i] = row[i].width || row[i].text.length
 | |
|     else if (w === undefined) widths[i] = Math.max(unsetWidth, _minWidth(row[i]))
 | |
|   })
 | |
| 
 | |
|   return widths
 | |
| }
 | |
| 
 | |
| // calculates the minimum width of
 | |
| // a column, based on padding preferences.
 | |
| function _minWidth (col) {
 | |
|   var padding = col.padding || []
 | |
| 
 | |
|   return 1 + (padding[left] || 0) + (padding[right] || 0)
 | |
| }
 | |
| 
 | |
| module.exports = function (opts) {
 | |
|   opts = opts || {}
 | |
| 
 | |
|   return new UI({
 | |
|     width: (opts || {}).width || 80,
 | |
|     wrap: typeof opts.wrap === 'boolean' ? opts.wrap : true
 | |
|   })
 | |
| }
 |