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