|
|
- 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
- })
- }
|