astro-ghostcms/.pnpm-store/v3/files/72/736b9e7ce272b21d223b1ac0c86...

399 lines
12 KiB
Plaintext

const Style = require("./style.js")
const Format = require("./format.js")
/**
* Converts arrays of data into arrays of cell strings
* @param {TtyTable.Config} config
* @param {Array<Array<string>|object|TtyTable.Formatter>} inputData
* @returns {Array<string>}
*/
module.exports.stringifyData = (config, inputData) => {
const sections = {
header: [],
body: [],
footer: []
}
const marginLeft = Array(config.marginLeft + 1).join(" ")
const borderStyle = config.borderCharacters[config.borderStyle]
const borders = []
// support backwards compatibility cli-table's multiple constructor geometries
// @TODO deprecate and support only a single format
const constructorType = exports.getConstructorGeometry(inputData[0] || [], config)
const rows = exports.coerceConstructorGeometry(config, inputData, constructorType)
// when streaming values to tty-table, we don't want column widths to change
// from one rows set to the next, so we save the first set of widths and reuse
if (!global.columnWidths) {
global.columnWidths = {}
}
if (global.columnWidths[config.tableId]) {
config.table.columnWidths = global.columnWidths[config.tableId]
} else {
global.columnWidths[config.tableId] = config.table.columnWidths = Format.getColumnWidths(config, rows)
}
// stringify header cells
// hide header if no column names or if specified in config
switch (true) {
case (config.showHeader !== null && !config.showHeader): // explicitly false, hide
sections.header = []
break
case (config.showHeader === true): // explicitly true, show
case (!!config.table.header[0].find(obj => obj.value || obj.alias)): // atleast one named column, show header
sections.header = config.table.header.map(row => {
return exports.buildRow(config, row, "header", null, rows, inputData)
})
break
default: // no named columns, hide
sections.header = []
}
// stringify body cells
sections.body = rows.map((row, rowIndex) => {
return exports.buildRow(config, row, "body", rowIndex, rows, inputData)
})
// stringify footer cells
sections.footer = (config.table.footer instanceof Array && config.table.footer.length > 0) ? [config.table.footer] : []
sections.footer = sections.footer.map(row => {
return exports.buildRow(config, row, "footer", null, rows, inputData)
})
// apply borders
// 0=top, 1=middle, 2=bottom
for (let a = 0; a < 3; a++) {
// add left border
borders[a] = borderStyle[a].l
// add joined borders for each column
config.table.columnWidths.forEach((columnWidth, index, arr) => {
// Math.max because otherwise columns 1 wide wont have horizontal border
borders[a] += Array(Math.max(columnWidth, 2)).join(borderStyle[a].h)
borders[a] += ((index + 1 < arr.length) ? borderStyle[a].j : "")
})
// add right border
borders[a] += borderStyle[a].r
// no trailing space on footer
borders[a] = (a < 2) ? `${marginLeft + borders[a]}\n` : marginLeft + borders[a]
}
// top horizontal border
let output = borders[0]
// for each section (header,body,footer)
Object.keys(sections).forEach((p, i) => {
// for each row in the section
while (sections[p].length) {
const row = sections[p].shift()
// if(row.length === 0) {break}
row.forEach(line => {
// vertical row borders
output = `${output
+ marginLeft
// left vertical border
+ borderStyle[1].v
// join cells on vertical border
+ line.join(borderStyle[1].v)
// right vertical border
+ borderStyle[1].v
// end of line
}\n`
})
// bottom horizontal row border
switch (true) {
// skip if end of body and no footer
case (sections[p].length === 0
&& i === 1
&& sections.footer.length === 0):
break
// skip if end of footer
case (sections[p].length === 0
&& i === 2):
break
// skip if compact
case (config.compact && p === "body" && !row.empty):
break
// skip if border style is "none"
case (config.borderStyle === "none" && config.compact):
break
default:
output += borders[1]
}
}
})
// bottom horizontal border
output += borders[2]
const finalOutput = Array(config.marginTop + 1).join("\n") + output
// record the height of the output
config.height = finalOutput.split(/\r\n|\r|\n/).length
return finalOutput
}
module.exports.buildRow = (config, row, rowType, rowIndex, rowData, inputData) => {
let minRowHeight = 0
// tag row as empty if empty, used for `compact` option
if (row.length === 0 && config.compact) {
row.empty = true
return row
}
// force row to have correct number of columns
const lengthDifference = config.table.columnWidths.length - row.length
if (lengthDifference > 0) {
// array (row) lacks elements, add until equal
row = row.concat(Array.apply(null, new Array(lengthDifference)).map(() => null))
} else if (lengthDifference < 0) {
// array (row) has too many elements, remove until equal
row.length = config.table.columnWidths.length
}
// convert each element in row to cell format
row = row.map((elem, elemIndex) => {
const cell = exports.buildCell(config, elem, elemIndex, rowType, rowIndex, rowData, inputData)
minRowHeight = (minRowHeight < cell.length) ? cell.length : minRowHeight
return cell
})
// apply top and bottom padding to row
minRowHeight = (rowType === "header") ? minRowHeight
: minRowHeight + (config.paddingBottom + config.paddingTop)
const linedRow = Array.apply(null, { length: minRowHeight })
.map(Function.call, () => [])
row.forEach(function (cell, a) {
const whitespace = Array(config.table.columnWidths[a]).join(" ")
if (rowType === "body") {
// add whitespace for top padding
for (let i = 0; i < config.paddingTop; i++) {
cell.unshift(whitespace)
}
// add whitespace for bottom padding
for (let i = 0; i < config.paddingBottom; i++) {
cell.push(whitespace)
}
}
// a `row` is divided by columns (horizontally)
// a `linedRow` becomes the row divided instead into an array of vertical lines
// each nested line divided by columns
for (let i = 0; i < minRowHeight; i++) {
linedRow[i].push((typeof cell[i] !== "undefined")
? cell[i] : whitespace)
}
})
return linedRow
}
module.exports.buildCell = (config, elem, columnIndex, rowType, rowIndex, rowData, inputData) => {
let cellValue = null
const cellOptions = Object.assign(
{ reset: false },
config,
(rowType === "body") ? config.columnSettings[columnIndex] : {}, // ignore columnSettings for footer
(typeof elem === "object") ? elem : {}
)
if (rowType === "header") {
config.table.columns.push(cellOptions)
cellValue = cellOptions.alias || cellOptions.value || ""
} else {
// set cellValue
switch (true) {
case (typeof elem === "undefined" || elem === null):
// replace undefined/null elem values with placeholder
cellValue = (config.errorOnNull) ? config.defaultErrorValue : config.defaultValue
// @TODO add to elem defaults
cellOptions.isNull = true
break
case (typeof elem === "object" && elem !== null && typeof elem.value !== "undefined"):
cellValue = elem.value
break
case (typeof elem === "function"):
cellValue = elem.bind({
configure: function (object) {
return Object.assign(cellOptions, object)
},
style: Style.style,
resetStyle: Style.resetStyle
})(
(!cellOptions.isNull) ? cellValue : "",
columnIndex,
rowIndex,
rowData,
inputData
)
break
default:
// elem is assumed to be a scalar
cellValue = elem
}
// run formatter
if (typeof cellOptions.formatter === "function") {
cellValue = cellOptions.formatter
.bind({
configure: function (object) {
return Object.assign(cellOptions, object)
},
style: Style.style,
resetStyle: Style.resetStyle
})(
(!cellOptions.isNull) ? cellValue : "",
columnIndex,
rowIndex,
rowData,
inputData
)
}
}
// colorize cellValue
// we don't want the formatter to pass a styled cell value with ANSI codes
// (in case user wants to do math or string operations to cell value), so
// we apply default styles to the cell after it runs through the formatter
// and omit those default styles if the user applied `this.resetStyle`
if (!cellOptions.reset) {
cellValue = Style.colorizeCell(cellValue, cellOptions, rowType)
}
// textwrap cellValue
const { cell, innerWidth } = Format.wrapCellText(cellOptions, cellValue, columnIndex, cellOptions, rowType)
if (rowType === "header") {
config.table.columnInnerWidths.push(innerWidth)
}
return cell
}
/**
* Check for a backwards compatible (cli-table) constructor
*/
module.exports.getConstructorGeometry = (row, config) => {
let type
// rows passed as an object
if (typeof row === "object" && !(row instanceof Array)) {
const keys = Object.keys(row)
if (config.adapter === "automattic") {
// detected cross table
const key = keys[0]
if (row[key] instanceof Array) {
type = "automattic-cross"
} else {
// detected vertical table
type = "automattic-vertical"
}
} else {
// detected horizontal table
type = "o-horizontal"
}
} else {
// rows passed as an array
type = "a-horizontal"
}
return type
}
/**
* Coerce backwards compatible constructor styles
*/
module.exports.coerceConstructorGeometry = (config, rows, constructorType) => {
let output = []
switch (constructorType) {
case ("automattic-cross"):
// assign header styles to first column
config.columnSettings[0] = config.columnSettings[0] || {}
config.columnSettings[0].color = config.headerColor
output = rows.map(obj => {
const arr = []
const key = Object.keys(obj)[0]
arr.push(key)
return arr.concat(obj[key])
})
break
case ("automattic-vertical"):
// assign header styles to first column
config.columnSettings[0] = config.columnSettings[0] || {}
config.columnSettings[0].color = config.headerColor
output = rows.map(function (value) {
const key = Object.keys(value)[0]
return [key, value[key]]
})
break
case ("o-horizontal"):
// cell property names are specified in header columns
if (config.table.header[0].length
&& config.table.header[0].every(obj => obj.value)) {
output = rows.map(row => config.table.header[0]
.map(obj => row[obj.value]))
} // eslint-disable-line brace-style
// no property names given, default to object property order
else {
output = rows.map(obj => Object.values(obj))
}
break
case ("a-horizontal"):
output = rows
break
default:
}
return output
}
// @TODO For rotating horizontal data into a vertical table
// assumes all rows are same length
// module.exports.verticalizeMatrix = (config, inputArray) => {
//
// // grow to # arrays equal to number of columns in input array
// let outputArray = []
// let headers = config.table.columns
//
// // create a row for each heading, and prepend the row
// // with the heading name
// headers.forEach(name => outputArray.push([name]))
//
// inputArray.forEach(row => {
// row.forEach((element, index) => outputArray[index].push(element))
// })
//
// return outputArray
// }