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

247 lines
7.9 KiB

"use strict"
var wcwidth = require('./width')
var utils = require('./utils')
var padRight = utils.padRight
var padCenter = utils.padCenter
var padLeft = utils.padLeft
var splitIntoLines = utils.splitIntoLines
var splitLongWords = utils.splitLongWords
var truncateString = utils.truncateString
var DEFAULTS = {
maxWidth: Infinity,
minWidth: 0,
columnSplitter: ' ',
truncate: false,
truncateMarker: '…',
preserveNewLines: false,
paddingChr: ' ',
showHeaders: true,
headingTransform: function(key) {
return key.toUpperCase()
},
dataTransform: function(cell, column, index) {
return cell
}
}
module.exports = function(items, options) {
options = options || {}
var columnConfigs = options.config || {}
delete options.config // remove config so doesn't appear on every column.
var maxLineWidth = options.maxLineWidth || Infinity
delete options.maxLineWidth // this is a line control option, don't pass it to column
// Option defaults inheritance:
// options.config[columnName] => options => DEFAULTS
options = mixin(options, DEFAULTS)
options.config = options.config || Object.create(null)
options.spacing = options.spacing || '\n' // probably useless
options.preserveNewLines = !!options.preserveNewLines
options.showHeaders = !!options.showHeaders;
options.columns = options.columns || options.include // alias include/columns, prefer columns if supplied
var columnNames = options.columns || [] // optional user-supplied columns to include
items = toArray(items, columnNames)
// if not suppled column names, automatically determine columns from data keys
if (!columnNames.length) {
items.forEach(function(item) {
for (var columnName in item) {
if (columnNames.indexOf(columnName) === -1) columnNames.push(columnName)
}
})
}
// initialize column defaults (each column inherits from options.config)
var columns = columnNames.reduce(function(columns, columnName) {
var column = Object.create(options)
columns[columnName] = mixin(column, columnConfigs[columnName])
return columns
}, Object.create(null))
// sanitize column settings
columnNames.forEach(function(columnName) {
var column = columns[columnName]
column.maxWidth = Math.ceil(column.maxWidth)
column.minWidth = Math.ceil(column.minWidth)
column.truncate = !!column.truncate
})
// sanitize data
items = items.map(function(item) {
var result = Object.create(null)
columnNames.forEach(function(columnName) {
// null/undefined -> ''
result[columnName] = item[columnName] != null ? item[columnName] : ''
// toString everything
result[columnName] = '' + result[columnName]
if (columns[columnName].preserveNewLines) {
// merge non-newline whitespace chars
result[columnName] = result[columnName].replace(/[^\S\n]/gmi, ' ')
} else {
// merge all whitespace chars
result[columnName] = result[columnName].replace(/\s/gmi, ' ')
}
})
return result
})
// transform data cells
columnNames.forEach(function(columnName) {
var column = columns[columnName]
items = items.map(function(item, index) {
item[columnName] = column.dataTransform(item[columnName], column, index)
return item
})
})
// add headers
var headers = {}
if(options.showHeaders) {
columnNames.forEach(function(columnName) {
var column = columns[columnName]
headers[columnName] = column.headingTransform(columnName)
})
items.unshift(headers)
}
// get actual max-width between min & max
// based on length of data in columns
columnNames.forEach(function(columnName) {
var column = columns[columnName]
column.width = items.map(function(item) {
return item[columnName]
}).reduce(function(min, cur) {
return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))))
}, 0)
})
// split long words so they can break onto multiple lines
columnNames.forEach(function(columnName) {
var column = columns[columnName]
items = items.map(function(item) {
item[columnName] = splitLongWords(item[columnName], column.width, column.truncateMarker)
return item
})
})
// wrap long lines. each item is now an array of lines.
columnNames.forEach(function(columnName) {
var column = columns[columnName]
items = items.map(function(item, index) {
var cell = item[columnName]
item[columnName] = splitIntoLines(cell, column.width)
// if truncating required, only include first line + add truncation char
if (column.truncate && item[columnName].length > 1) {
item[columnName] = splitIntoLines(cell, column.width - wcwidth(column.truncateMarker))
var firstLine = item[columnName][0]
if (!endsWith(firstLine, column.truncateMarker)) item[columnName][0] += column.truncateMarker
item[columnName] = item[columnName].slice(0, 1)
}
return item
})
})
// recalculate column widths from truncated output/lines
columnNames.forEach(function(columnName) {
var column = columns[columnName]
column.width = items.map(function(item) {
return item[columnName].reduce(function(min, cur) {
return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))))
}, 0)
}).reduce(function(min, cur) {
return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur)))
}, 0)
})
var rows = createRows(items, columns, columnNames, options.paddingChr) // merge lines into rows
// conceive output
return rows.reduce(function(output, row) {
return output.concat(row.reduce(function(rowOut, line) {
return rowOut.concat(line.join(options.columnSplitter))
}, []))
}, []).map(function(line) {
return truncateString(line, maxLineWidth)
}).join(options.spacing)
}
/**
* Convert wrapped lines into rows with padded values.
*
* @param Array items data to process
* @param Array columns column width settings for wrapping
* @param Array columnNames column ordering
* @return Array items wrapped in arrays, corresponding to lines
*/
function createRows(items, columns, columnNames, paddingChr) {
return items.map(function(item) {
var row = []
var numLines = 0
columnNames.forEach(function(columnName) {
numLines = Math.max(numLines, item[columnName].length)
})
// combine matching lines of each rows
for (var i = 0; i < numLines; i++) {
row[i] = row[i] || []
columnNames.forEach(function(columnName) {
var column = columns[columnName]
var val = item[columnName][i] || '' // || '' ensures empty columns get padded
if (column.align == 'right') row[i].push(padLeft(val, column.width, paddingChr))
else if (column.align == 'center') row[i].push(padCenter(val, column.width, paddingChr))
else row[i].push(padRight(val, column.width, paddingChr))
})
}
return row
})
}
/**
* Generic source->target mixin.
* Copy properties from `source` into `target` if target doesn't have them.
* Destructive. Modifies `target`.
*
* @param target Object target for mixin properties.
* @param source Object source of mixin properties.
* @return Object `target` after mixin applied.
*/
function mixin(target, source) {
source = source || {}
for (var key in source) {
if (target.hasOwnProperty(key)) continue
target[key] = source[key]
}
return target
}
/**
* Adapted from String.prototype.endsWith polyfill.
*/
function endsWith(target, searchString, position) {
position = position || target.length;
position = position - searchString.length;
var lastIndex = target.lastIndexOf(searchString);
return lastIndex !== -1 && lastIndex === position;
}
function toArray(items, columnNames) {
if (Array.isArray(items)) return items
var rows = []
for (var key in items) {
var item = {}
item[columnNames[0] || 'key'] = key
item[columnNames[1] || 'value'] = items[key]
rows.push(item)
}
return rows
}