You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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