123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- 'use strict'
- const stringWidth = require('string-width')
- const stripAnsi = require('strip-ansi')
- const wrap = require('wrap-ansi')
- const align = {
- right: alignRight,
- center: alignCenter
- }
- const top = 0
- const right = 1
- const bottom = 2
- const left = 3
- class UI {
- constructor (opts) {
- this.width = opts.width
- this.wrap = opts.wrap
- this.rows = []
- }
- span (...args) {
- const cols = this.div(...args)
- cols.span = true
- }
- resetOutput () {
- this.rows = []
- }
- div (...args) {
- if (args.length === 0) {
- this.div('')
- }
- if (this.wrap && this._shouldApplyLayoutDSL(...args)) {
- return this._applyLayoutDSL(args[0])
- }
- const cols = args.map(arg => {
- if (typeof arg === 'string') {
- return this._colFromString(arg)
- }
- return arg
- })
- this.rows.push(cols)
- return cols
- }
- _shouldApplyLayoutDSL (...args) {
- return args.length === 1 && typeof args[0] === 'string' &&
- /[\t\n]/.test(args[0])
- }
- _applyLayoutDSL (str) {
- const rows = str.split('\n').map(row => row.split('\t'))
- let 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(columns => {
- 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(columns => {
- this.div(...columns.map((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]
- }
- _colFromString (text) {
- return {
- text,
- padding: this._measurePadding(text)
- }
- }
- _measurePadding (str) {
- // measure padding without ansi escape codes
- const noAnsi = stripAnsi(str)
- return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length]
- }
- toString () {
- const lines = []
- this.rows.forEach(row => {
- this.rowToString(row, lines)
- })
- // don't display any lines with the
- // hidden flag set.
- return lines
- .filter(line => !line.hidden)
- .map(line => line.text)
- .join('\n')
- }
- rowToString (row, lines) {
- this._rasterize(row).forEach((rrow, r) => {
- let str = ''
- rrow.forEach((col, c) => {
- const { width } = row[c] // the width with padding.
- const wrapWidth = this._negatePadding(row[c]) // the width without padding.
- let ts = col // temporary string used during alignment/padding.
- if (wrapWidth > stringWidth(col)) {
- ts += ' '.repeat(wrapWidth - stringWidth(col))
- }
- // 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 += ' '.repeat(width - stringWidth(ts) - 1)
- }
- }
- // apply border and padding to string.
- const padding = row[c].padding || [0, 0, 0, 0]
- if (padding[left]) {
- str += ' '.repeat(padding[left])
- }
- str += addBorder(row[c], ts, '| ')
- str += ts
- str += addBorder(row[c], ts, ' |')
- if (padding[right]) {
- str += ' '.repeat(padding[right])
- }
- // 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
- }
- // if the full 'source' can render in
- // the target line, do so.
- _renderInline (source, previousLine) {
- const leadingWhitespace = source.match(/^ */)[0].length
- const target = previousLine.text
- const 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() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft()
- }
- _rasterize (row) {
- const rrows = []
- const widths = this._columnWidths(row)
- let wrapped
- // word wrap all columns, and create
- // a data-structure that is easy to rasterize.
- row.forEach((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('.' + '-'.repeat(this._negatePadding(col) + 2) + '.')
- wrapped.push("'" + '-'.repeat(this._negatePadding(col) + 2) + "'")
- }
- // add top and bottom padding.
- if (col.padding) {
- wrapped.unshift(...new Array(col.padding[top] || 0).fill(''))
- wrapped.push(...new Array(col.padding[bottom] || 0).fill(''))
- }
- wrapped.forEach((str, r) => {
- if (!rrows[r]) {
- rrows.push([])
- }
- const rrow = rrows[r]
- for (let i = 0; i < c; i++) {
- if (rrow[i] === undefined) {
- rrow.push('')
- }
- }
- rrow.push(str)
- })
- })
- return rrows
- }
- _negatePadding (col) {
- let wrapWidth = col.width
- if (col.padding) {
- wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0)
- }
- if (col.border) {
- wrapWidth -= 4
- }
- return wrapWidth
- }
- _columnWidths (row) {
- if (!this.wrap) {
- return row.map(col => {
- return col.width || stringWidth(col.text)
- })
- }
- let unset = row.length
- let remainingWidth = this.width
- // column widths can be set in config.
- const widths = row.map(col => {
- if (col.width) {
- unset--
- remainingWidth -= col.width
- return col.width
- }
- return undefined
- })
- // any unset widths should be calculated.
- const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0
- return widths.map((w, i) => {
- if (w === undefined) {
- return Math.max(unsetWidth, _minWidth(row[i]))
- }
- return w
- })
- }
- }
- function addBorder (col, ts, style) {
- if (col.border) {
- if (/[.']-+[.']/.test(ts)) {
- return ''
- }
- if (ts.trim().length !== 0) {
- return style
- }
- return ' '
- }
- return ''
- }
- // calculates the minimum width of
- // a column, based on padding preferences.
- function _minWidth (col) {
- const padding = col.padding || []
- const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0)
- if (col.border) {
- return minWidth + 4
- }
- return minWidth
- }
- function getWindowWidth () {
- /* istanbul ignore next: depends on terminal */
- if (typeof process === 'object' && process.stdout && process.stdout.columns) {
- return process.stdout.columns
- }
- }
- function alignRight (str, width) {
- str = str.trim()
- const strWidth = stringWidth(str)
- if (strWidth < width) {
- return ' '.repeat(width - strWidth) + str
- }
- return str
- }
- function alignCenter (str, width) {
- str = str.trim()
- const strWidth = stringWidth(str)
- /* istanbul ignore next */
- if (strWidth >= width) {
- return str
- }
- return ' '.repeat((width - strWidth) >> 1) + str
- }
- module.exports = function (opts = {}) {
- return new UI({
- width: opts.width || getWindowWidth() || /* istanbul ignore next */ 80,
- wrap: opts.wrap !== false
- })
- }
|