Projekt

Obecné

Profil

Stáhnout (8.08 KB) Statistiky
| Větev: | Revize:
1
var stringWidth = require('string-width')
2
var stripAnsi = require('strip-ansi')
3
var wrap = require('wrap-ansi')
4
var align = {
5
  right: alignRight,
6
  center: alignCenter
7
}
8
var top = 0
9
var right = 1
10
var bottom = 2
11
var left = 3
12

    
13
function UI (opts) {
14
  this.width = opts.width
15
  this.wrap = opts.wrap
16
  this.rows = []
17
}
18

    
19
UI.prototype.span = function () {
20
  var cols = this.div.apply(this, arguments)
21
  cols.span = true
22
}
23

    
24
UI.prototype.resetOutput = function () {
25
  this.rows = []
26
}
27

    
28
UI.prototype.div = function () {
29
  if (arguments.length === 0) this.div('')
30
  if (this.wrap && this._shouldApplyLayoutDSL.apply(this, arguments)) {
31
    return this._applyLayoutDSL(arguments[0])
32
  }
33

    
34
  var cols = []
35

    
36
  for (var i = 0, arg; (arg = arguments[i]) !== undefined; i++) {
37
    if (typeof arg === 'string') cols.push(this._colFromString(arg))
38
    else cols.push(arg)
39
  }
40

    
41
  this.rows.push(cols)
42
  return cols
43
}
44

    
45
UI.prototype._shouldApplyLayoutDSL = function () {
46
  return arguments.length === 1 && typeof arguments[0] === 'string' &&
47
    /[\t\n]/.test(arguments[0])
48
}
49

    
50
UI.prototype._applyLayoutDSL = function (str) {
51
  var _this = this
52
  var rows = str.split('\n')
53
  var leftColumnWidth = 0
54

    
55
  // simple heuristic for layout, make sure the
56
  // second column lines up along the left-hand.
57
  // don't allow the first column to take up more
58
  // than 50% of the screen.
59
  rows.forEach(function (row) {
60
    var columns = row.split('\t')
61
    if (columns.length > 1 && stringWidth(columns[0]) > leftColumnWidth) {
62
      leftColumnWidth = Math.min(
63
        Math.floor(_this.width * 0.5),
64
        stringWidth(columns[0])
65
      )
66
    }
67
  })
68

    
69
  // generate a table:
70
  //  replacing ' ' with padding calculations.
71
  //  using the algorithmically generated width.
72
  rows.forEach(function (row) {
73
    var columns = row.split('\t')
74
    _this.div.apply(_this, columns.map(function (r, i) {
75
      return {
76
        text: r.trim(),
77
        padding: _this._measurePadding(r),
78
        width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
79
      }
80
    }))
81
  })
82

    
83
  return this.rows[this.rows.length - 1]
84
}
85

    
86
UI.prototype._colFromString = function (str) {
87
  return {
88
    text: str,
89
    padding: this._measurePadding(str)
90
  }
91
}
92

    
93
UI.prototype._measurePadding = function (str) {
94
  // measure padding without ansi escape codes
95
  var noAnsi = stripAnsi(str)
96
  return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length]
97
}
98

    
99
UI.prototype.toString = function () {
100
  var _this = this
101
  var lines = []
102

    
103
  _this.rows.forEach(function (row, i) {
104
    _this.rowToString(row, lines)
105
  })
106

    
107
  // don't display any lines with the
108
  // hidden flag set.
109
  lines = lines.filter(function (line) {
110
    return !line.hidden
111
  })
112

    
113
  return lines.map(function (line) {
114
    return line.text
115
  }).join('\n')
116
}
117

    
118
UI.prototype.rowToString = function (row, lines) {
119
  var _this = this
120
  var padding
121
  var rrows = this._rasterize(row)
122
  var str = ''
123
  var ts
124
  var width
125
  var wrapWidth
126

    
127
  rrows.forEach(function (rrow, r) {
128
    str = ''
129
    rrow.forEach(function (col, c) {
130
      ts = '' // temporary string used during alignment/padding.
131
      width = row[c].width // the width with padding.
132
      wrapWidth = _this._negatePadding(row[c]) // the width without padding.
133

    
134
      ts += col
135

    
136
      for (var i = 0; i < wrapWidth - stringWidth(col); i++) {
137
        ts += ' '
138
      }
139

    
140
      // align the string within its column.
141
      if (row[c].align && row[c].align !== 'left' && _this.wrap) {
142
        ts = align[row[c].align](ts, wrapWidth)
143
        if (stringWidth(ts) < wrapWidth) ts += new Array(width - stringWidth(ts)).join(' ')
144
      }
145

    
146
      // apply border and padding to string.
147
      padding = row[c].padding || [0, 0, 0, 0]
148
      if (padding[left]) str += new Array(padding[left] + 1).join(' ')
149
      str += addBorder(row[c], ts, '| ')
150
      str += ts
151
      str += addBorder(row[c], ts, ' |')
152
      if (padding[right]) str += new Array(padding[right] + 1).join(' ')
153

    
154
      // if prior row is span, try to render the
155
      // current row on the prior line.
156
      if (r === 0 && lines.length > 0) {
157
        str = _this._renderInline(str, lines[lines.length - 1])
158
      }
159
    })
160

    
161
    // remove trailing whitespace.
162
    lines.push({
163
      text: str.replace(/ +$/, ''),
164
      span: row.span
165
    })
166
  })
167

    
168
  return lines
169
}
170

    
171
function addBorder (col, ts, style) {
172
  if (col.border) {
173
    if (/[.']-+[.']/.test(ts)) return ''
174
    else if (ts.trim().length) return style
175
    else return '  '
176
  }
177
  return ''
178
}
179

    
180
// if the full 'source' can render in
181
// the target line, do so.
182
UI.prototype._renderInline = function (source, previousLine) {
183
  var leadingWhitespace = source.match(/^ */)[0].length
184
  var target = previousLine.text
185
  var targetTextWidth = stringWidth(target.trimRight())
186

    
187
  if (!previousLine.span) return source
188

    
189
  // if we're not applying wrapping logic,
190
  // just always append to the span.
191
  if (!this.wrap) {
192
    previousLine.hidden = true
193
    return target + source
194
  }
195

    
196
  if (leadingWhitespace < targetTextWidth) return source
197

    
198
  previousLine.hidden = true
199

    
200
  return target.trimRight() + new Array(leadingWhitespace - targetTextWidth + 1).join(' ') + source.trimLeft()
201
}
202

    
203
UI.prototype._rasterize = function (row) {
204
  var _this = this
205
  var i
206
  var rrow
207
  var rrows = []
208
  var widths = this._columnWidths(row)
209
  var wrapped
210

    
211
  // word wrap all columns, and create
212
  // a data-structure that is easy to rasterize.
213
  row.forEach(function (col, c) {
214
    // leave room for left and right padding.
215
    col.width = widths[c]
216
    if (_this.wrap) wrapped = wrap(col.text, _this._negatePadding(col), { hard: true }).split('\n')
217
    else wrapped = col.text.split('\n')
218

    
219
    if (col.border) {
220
      wrapped.unshift('.' + new Array(_this._negatePadding(col) + 3).join('-') + '.')
221
      wrapped.push("'" + new Array(_this._negatePadding(col) + 3).join('-') + "'")
222
    }
223

    
224
    // add top and bottom padding.
225
    if (col.padding) {
226
      for (i = 0; i < (col.padding[top] || 0); i++) wrapped.unshift('')
227
      for (i = 0; i < (col.padding[bottom] || 0); i++) wrapped.push('')
228
    }
229

    
230
    wrapped.forEach(function (str, r) {
231
      if (!rrows[r]) rrows.push([])
232

    
233
      rrow = rrows[r]
234

    
235
      for (var i = 0; i < c; i++) {
236
        if (rrow[i] === undefined) rrow.push('')
237
      }
238
      rrow.push(str)
239
    })
240
  })
241

    
242
  return rrows
243
}
244

    
245
UI.prototype._negatePadding = function (col) {
246
  var wrapWidth = col.width
247
  if (col.padding) wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0)
248
  if (col.border) wrapWidth -= 4
249
  return wrapWidth
250
}
251

    
252
UI.prototype._columnWidths = function (row) {
253
  var _this = this
254
  var widths = []
255
  var unset = row.length
256
  var unsetWidth
257
  var remainingWidth = this.width
258

    
259
  // column widths can be set in config.
260
  row.forEach(function (col, i) {
261
    if (col.width) {
262
      unset--
263
      widths[i] = col.width
264
      remainingWidth -= col.width
265
    } else {
266
      widths[i] = undefined
267
    }
268
  })
269

    
270
  // any unset widths should be calculated.
271
  if (unset) unsetWidth = Math.floor(remainingWidth / unset)
272
  widths.forEach(function (w, i) {
273
    if (!_this.wrap) widths[i] = row[i].width || stringWidth(row[i].text)
274
    else if (w === undefined) widths[i] = Math.max(unsetWidth, _minWidth(row[i]))
275
  })
276

    
277
  return widths
278
}
279

    
280
// calculates the minimum width of
281
// a column, based on padding preferences.
282
function _minWidth (col) {
283
  var padding = col.padding || []
284
  var minWidth = 1 + (padding[left] || 0) + (padding[right] || 0)
285
  if (col.border) minWidth += 4
286
  return minWidth
287
}
288

    
289
function getWindowWidth () {
290
  if (typeof process === 'object' && process.stdout && process.stdout.columns) return process.stdout.columns
291
}
292

    
293
function alignRight (str, width) {
294
  str = str.trim()
295
  var padding = ''
296
  var strWidth = stringWidth(str)
297

    
298
  if (strWidth < width) {
299
    padding = new Array(width - strWidth + 1).join(' ')
300
  }
301

    
302
  return padding + str
303
}
304

    
305
function alignCenter (str, width) {
306
  str = str.trim()
307
  var padding = ''
308
  var strWidth = stringWidth(str.trim())
309

    
310
  if (strWidth < width) {
311
    padding = new Array(parseInt((width - strWidth) / 2, 10) + 1).join(' ')
312
  }
313

    
314
  return padding + str
315
}
316

    
317
module.exports = function (opts) {
318
  opts = opts || {}
319

    
320
  return new UI({
321
    width: (opts || {}).width || getWindowWidth() || 80,
322
    wrap: typeof opts.wrap === 'boolean' ? opts.wrap : true
323
  })
324
}
(3-3/4)