325 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			325 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | var stringWidth = require('string-width') | ||
|  | var stripAnsi = require('strip-ansi') | ||
|  | var wrap = require('wrap-ansi') | ||
|  | var align = { | ||
|  |   right: alignRight, | ||
|  |   center: alignCenter | ||
|  | } | ||
|  | var top = 0 | ||
|  | var right = 1 | ||
|  | var bottom = 2 | ||
|  | var 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.resetOutput = function () { | ||
|  |   this.rows = [] | ||
|  | } | ||
|  | 
 | ||
|  | 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 | ||
|  |   var rows = str.split('\n') | ||
|  |   var 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 && stringWidth(columns[0]) > leftColumnWidth) { | ||
|  |       leftColumnWidth = Math.min( | ||
|  |         Math.floor(_this.width * 0.5), | ||
|  |         stringWidth(columns[0]) | ||
|  |       ) | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   // 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: _this._measurePadding(r), | ||
|  |         width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined | ||
|  |       } | ||
|  |     })) | ||
|  |   }) | ||
|  | 
 | ||
|  |   return this.rows[this.rows.length - 1] | ||
|  | } | ||
|  | 
 | ||
|  | UI.prototype._colFromString = function (str) { | ||
|  |   return { | ||
|  |     text: str, | ||
|  |     padding: this._measurePadding(str) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | UI.prototype._measurePadding = function (str) { | ||
|  |   // measure padding without ansi escape codes
 | ||
|  |   var noAnsi = stripAnsi(str) | ||
|  |   return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length] | ||
|  | } | ||
|  | 
 | ||
|  | UI.prototype.toString = function () { | ||
|  |   var _this = this | ||
|  |   var 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 | ||
|  |   var padding | ||
|  |   var rrows = this._rasterize(row) | ||
|  |   var str = '' | ||
|  |   var ts | ||
|  |   var width | ||
|  |   var 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.
 | ||
|  | 
 | ||
|  |       ts += col | ||
|  | 
 | ||
|  |       for (var i = 0; i < wrapWidth - stringWidth(col); i++) { | ||
|  |         ts += ' ' | ||
|  |       } | ||
|  | 
 | ||
|  |       // align the string within its column.
 | ||
|  |       if (row[c].align && row[c].align !== 'left' && _this.wrap) { | ||
|  |         ts = align[row[c].align](ts, wrapWidth) | ||
|  |         if (stringWidth(ts) < wrapWidth) ts += new Array(width - stringWidth(ts)).join(' ') | ||
|  |       } | ||
|  | 
 | ||
|  |       // apply border and padding to string.
 | ||
|  |       padding = row[c].padding || [0, 0, 0, 0] | ||
|  |       if (padding[left]) str += new Array(padding[left] + 1).join(' ') | ||
|  |       str += addBorder(row[c], ts, '| ') | ||
|  |       str += ts | ||
|  |       str += addBorder(row[c], ts, ' |') | ||
|  |       if (padding[right]) str += new Array(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]) | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     // remove trailing whitespace.
 | ||
|  |     lines.push({ | ||
|  |       text: str.replace(/ +$/, ''), | ||
|  |       span: row.span | ||
|  |     }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   return lines | ||
|  | } | ||
|  | 
 | ||
|  | function addBorder (col, ts, style) { | ||
|  |   if (col.border) { | ||
|  |     if (/[.']-+[.']/.test(ts)) return '' | ||
|  |     else if (ts.trim().length) return style | ||
|  |     else return '  ' | ||
|  |   } | ||
|  |   return '' | ||
|  | } | ||
|  | 
 | ||
|  | // if the full 'source' can render in
 | ||
|  | // the target line, do so.
 | ||
|  | UI.prototype._renderInline = function (source, previousLine) { | ||
|  |   var leadingWhitespace = source.match(/^ */)[0].length | ||
|  |   var target = previousLine.text | ||
|  |   var targetTextWidth = stringWidth(target.trimRight()) | ||
|  | 
 | ||
|  |   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 | ||
|  |   } | ||
|  | 
 | ||
|  |   if (leadingWhitespace < targetTextWidth) return source | ||
|  | 
 | ||
|  |   previousLine.hidden = true | ||
|  | 
 | ||
|  |   return target.trimRight() + new Array(leadingWhitespace - targetTextWidth + 1).join(' ') + source.trimLeft() | ||
|  | } | ||
|  | 
 | ||
|  | UI.prototype._rasterize = function (row) { | ||
|  |   var _this = this | ||
|  |   var i | ||
|  |   var rrow | ||
|  |   var rrows = [] | ||
|  |   var widths = this._columnWidths(row) | ||
|  |   var 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(col.text, _this._negatePadding(col), { hard: true }).split('\n') | ||
|  |     else wrapped = col.text.split('\n') | ||
|  | 
 | ||
|  |     if (col.border) { | ||
|  |       wrapped.unshift('.' + new Array(_this._negatePadding(col) + 3).join('-') + '.') | ||
|  |       wrapped.push("'" + new Array(_this._negatePadding(col) + 3).join('-') + "'") | ||
|  |     } | ||
|  | 
 | ||
|  |     // 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) | ||
|  |   if (col.border) wrapWidth -= 4 | ||
|  |   return wrapWidth | ||
|  | } | ||
|  | 
 | ||
|  | UI.prototype._columnWidths = function (row) { | ||
|  |   var _this = this | ||
|  |   var widths = [] | ||
|  |   var unset = row.length | ||
|  |   var unsetWidth | ||
|  |   var 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 || stringWidth(row[i].text) | ||
|  |     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 || [] | ||
|  |   var minWidth = 1 + (padding[left] || 0) + (padding[right] || 0) | ||
|  |   if (col.border) minWidth += 4 | ||
|  |   return minWidth | ||
|  | } | ||
|  | 
 | ||
|  | function getWindowWidth () { | ||
|  |   if (typeof process === 'object' && process.stdout && process.stdout.columns) return process.stdout.columns | ||
|  | } | ||
|  | 
 | ||
|  | function alignRight (str, width) { | ||
|  |   str = str.trim() | ||
|  |   var padding = '' | ||
|  |   var strWidth = stringWidth(str) | ||
|  | 
 | ||
|  |   if (strWidth < width) { | ||
|  |     padding = new Array(width - strWidth + 1).join(' ') | ||
|  |   } | ||
|  | 
 | ||
|  |   return padding + str | ||
|  | } | ||
|  | 
 | ||
|  | function alignCenter (str, width) { | ||
|  |   str = str.trim() | ||
|  |   var padding = '' | ||
|  |   var strWidth = stringWidth(str.trim()) | ||
|  | 
 | ||
|  |   if (strWidth < width) { | ||
|  |     padding = new Array(parseInt((width - strWidth) / 2, 10) + 1).join(' ') | ||
|  |   } | ||
|  | 
 | ||
|  |   return padding + str | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = function (opts) { | ||
|  |   opts = opts || {} | ||
|  | 
 | ||
|  |   return new UI({ | ||
|  |     width: (opts || {}).width || getWindowWidth() || 80, | ||
|  |     wrap: typeof opts.wrap === 'boolean' ? opts.wrap : true | ||
|  |   }) | ||
|  | } |