astro-ghostcms/.pnpm-store/v3/files/ab/e7a69e3adf79c2b79618078d940...

290 lines
7.6 KiB
Plaintext
Raw Normal View History

2024-02-14 14:10:47 +00:00
const breakword = require("breakword")
const stripansi = require("strip-ansi")
const wcwidth = require("wcwidth")
const flat = require("array.prototype.flat")
if (!Array.prototype.flat) flat.shim()
const ANSIPattern = [
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"
].join("|")
const ANSIRegex = new RegExp(ANSIPattern, "g")
const defaults = () => {
let obj = {}
obj.breakword = false
obj.input = [] // input string split by whitespace
obj.minWidth = 2 // fallback to if width set too narrow
obj.paddingLeft = 0
obj.paddingRight = 0
obj.errorChar = "<22>"
obj.returnFormat = "string" // or 'array'
obj.skipPadding = false // set to true when padding set too wide for line length
obj.splitAt = [" ", "\t"]
obj.trim = true
obj.width = 10
return obj
}
const calculateSpaceRemaining = function(lineLength, spacesUsed, config) {
return Math.max(lineLength - spacesUsed - config.paddingLeft - config.paddingRight, 0)
} // function to set starting line length
const validateInput = (text, options) => {
// options validation
let config = Object.assign({}, defaults(), options || {})
if (config.errorChar) {
// only allow a single errorChar
config.errorChar = config.errorChar.split("")[0]
// errorChar must not be wide character
if (wcwidth(config.errorChar) > 1)
throw new Error(`Error character cannot be a wide character (${config.errorChar})`)
}
// make sure correct sign on padding
config.paddingLeft = Math.abs(config.paddingLeft)
config.paddingRight = Math.abs(config.paddingRight)
let lineLength = config.width
- config.paddingLeft
- config.paddingRight
if(lineLength < config.minWidth) {
// skip padding if lineLength too narrow
config.skipPadding = true
lineLength = config.minWidth
}
// to trim or not to trim...
if(config.trim) {
text = text.trim()
}
return { text, config, lineLength }
}
const wrap = (input, options) => {
let { text, config, lineLength } = validateInput(input, options)
// array of characters split by whitespace and/or tabs
let words = []
if(!config.breakword) {
// break string into words
if(config.splitAt.indexOf("\t")!==-1) {
// split at both spaces and tabs
words = text.split(/ |\t/i)
} else{
// split at whitespace
words = text.split(" ")
}
} else {
// do not break string into words
words = [text]
}
// remove empty array elements
words = words.filter(val => {
if (val.length > 0) {
return true
}
})
// assume at least one line
let lines = [
[]
]
let spaceRemaining, splitIndex, word
let currentLine = 0 // index of current line in 'lines[]'
let spacesUsed = 0 // spaces used so far on current line
while(words.length > 0) {
spaceRemaining = calculateSpaceRemaining(lineLength, spacesUsed, config)
word = words.shift()
let wordLength = wcwidth(word)
switch(true) {
// too long for an empty line and is a single character
case(lineLength < wordLength && [...word].length === 1):
words.unshift(config.errorChar)
break
// too long for an empty line, must be broken between 2 lines
case(lineLength < wordLength):
// break it, then re-insert its parts into words
// so can loop back to re-handle each word
splitIndex = breakword(word, lineLength)
let splitWord = [...word]
words.unshift(splitWord.slice(0, splitIndex + 1).join(""))
words.splice(1, 0, splitWord.slice(splitIndex + 1).join("")) // +1 for substr fn
break
// not enough space remaining in line, must be wrapped to next line
case(spaceRemaining < wordLength):
// add a new line to our array of lines
lines.push([])
// note carriage to new line in counter
currentLine++
// reset the spacesUsed to 0
spacesUsed = 0
/* falls through */
// fits on current line
// eslint-disable-next-line
default:
// add word to line
lines[currentLine].push(word)
// reduce space remaining (add a space between words)
spacesUsed += wordLength + 1
}
}
lines = lines.map( line => {
// restore spaces to line
line = line.join(" ")
// add padding to ends of line
if(!config.skipPadding) {
line = Array(config.paddingLeft + 1).join(" ")
+ line
+ Array(config.paddingRight + 1).join(" ")
}
return line
})
return lines.join("\n")
}
const splitAnsiInput = (text) => {
// get start and end positions for matches
let matches = []
let textArr = [...text]
let textLength = textArr.length
/* eslint-disable */
while((result = ANSIRegex.exec(text)) !== null) {
matches.push({
start: result.index,
end: result.index + result[0].length,
match: result[0],
length: result[0].length
})
}
/* eslint-enable */
if (matches.length < 1) return [] // we have no ANSI escapes, we're done here
// add start and end positions for non matches
matches = matches.reduce((prev, curr) => {
// check if space exists between this and last match
// get end of previous match
let prevEnd = prev[prev.length -1]
if (prevEnd.end < curr.start) {
// insert placeholder
prev.push({
start: prevEnd.end,
end: curr.start,
length: curr.start - prevEnd.end,
expand: true
}, curr)
} else {
prev.push(curr)
}
return prev
}, [{start: 0, end: 0}])
.splice(1) // removes starting accumulator object
// add trailing match if necessary
let lastMatchEnd = matches[matches.length - 1].end
if (lastMatchEnd < textLength) {
matches.push({
start: lastMatchEnd,
end: textLength,
expand: true
})
}
let savedArr = matches.map(match => {
let value = text.substring(match.start, match.end)
return (match.expand) ? [...value] : [value]
}).flat(2)
return savedArr
}
const restoreANSI = (savedArr, processedArr) => {
return processedArr.map((char) => {
let result
if (char === "\n") {
result = [char]
} else {
// add everything saved before character match
let splicePoint = savedArr.findIndex(element => element === char ) + 1
result = savedArr.splice(0, splicePoint)
}
// add all following, consecutive closing tags in case linebreak inerted next
const ANSIClosePattern = "^\\x1b\\[([0-9]+)*m"
const ANSICloseRegex = new RegExp(ANSIClosePattern) // eslint-disable-line no-control-regex
const closeCodes = ["0", "21", "22", "23", "24", "25", "27", "28", "29", "39", "49", "54", "55"]
let match
while (savedArr.length && (match = savedArr[0].match(ANSICloseRegex))) {
if (!closeCodes.includes(match[1])) break
result.push(savedArr.shift())
}
return result.join("")
}).concat(savedArr)
}
module.exports = (input, options) => {
// process each existing line separately to respect existing line breaks
const processedLines = input.toString().split("\n").map( string => {
// save input ANSI escape codes to be restored later
const savedANSI = splitAnsiInput(string)
// strip ANSI
string = stripansi(string)
// add newlines to string
string = wrap(string, options)
// convert into array of characters
let charArr = [...string]
// restore input ANSI escape codes
charArr = (savedANSI.length > 0) ? restoreANSI(savedANSI, charArr) : charArr
// convert array of single characters into array of lines
let outArr = charArr.join("").split("\n")
return outArr
})
return processedLines.flat(2).join("\n")
}