Added test to ensure that release notes are parsed properly Updated to latest dependencies to fix security vulnerability Updated GH Actions Workflow to run test before updating tag Updated README.md to reflect proper whatsnew documentation.
189 lines
4.6 KiB
JavaScript
Executable File
189 lines
4.6 KiB
JavaScript
Executable File
'use strict';
|
|
const stringWidth = require('string-width');
|
|
const stripAnsi = require('strip-ansi');
|
|
const ansiStyles = require('ansi-styles');
|
|
|
|
const ESCAPES = new Set([
|
|
'\u001B',
|
|
'\u009B'
|
|
]);
|
|
|
|
const END_CODE = 39;
|
|
|
|
const wrapAnsi = code => `${ESCAPES.values().next().value}[${code}m`;
|
|
|
|
// Calculate the length of words split on ' ', ignoring
|
|
// the extra characters added by ansi escape codes
|
|
const wordLengths = string => string.split(' ').map(character => stringWidth(character));
|
|
|
|
// Wrap a long word across multiple rows
|
|
// Ansi escape codes do not count towards length
|
|
const wrapWord = (rows, word, columns) => {
|
|
const characters = [...word];
|
|
|
|
let insideEscape = false;
|
|
let visible = stringWidth(stripAnsi(rows[rows.length - 1]));
|
|
|
|
for (const [index, character] of characters.entries()) {
|
|
const characterLength = stringWidth(character);
|
|
|
|
if (visible + characterLength <= columns) {
|
|
rows[rows.length - 1] += character;
|
|
} else {
|
|
rows.push(character);
|
|
visible = 0;
|
|
}
|
|
|
|
if (ESCAPES.has(character)) {
|
|
insideEscape = true;
|
|
} else if (insideEscape && character === 'm') {
|
|
insideEscape = false;
|
|
continue;
|
|
}
|
|
|
|
if (insideEscape) {
|
|
continue;
|
|
}
|
|
|
|
visible += characterLength;
|
|
|
|
if (visible === columns && index < characters.length - 1) {
|
|
rows.push('');
|
|
visible = 0;
|
|
}
|
|
}
|
|
|
|
// It's possible that the last row we copy over is only
|
|
// ansi escape characters, handle this edge-case
|
|
if (!visible && rows[rows.length - 1].length > 0 && rows.length > 1) {
|
|
rows[rows.length - 2] += rows.pop();
|
|
}
|
|
};
|
|
|
|
// Trims spaces from a string ignoring invisible sequences
|
|
const stringVisibleTrimSpacesRight = str => {
|
|
const words = str.split(' ');
|
|
let last = words.length;
|
|
|
|
while (last > 0) {
|
|
if (stringWidth(words[last - 1]) > 0) {
|
|
break;
|
|
}
|
|
|
|
last--;
|
|
}
|
|
|
|
if (last === words.length) {
|
|
return str;
|
|
}
|
|
|
|
return words.slice(0, last).join(' ') + words.slice(last).join('');
|
|
};
|
|
|
|
// The wrap-ansi module can be invoked
|
|
// in either 'hard' or 'soft' wrap mode
|
|
//
|
|
// 'hard' will never allow a string to take up more
|
|
// than columns characters
|
|
//
|
|
// 'soft' allows long words to expand past the column length
|
|
const exec = (string, columns, options = {}) => {
|
|
if (options.trim !== false && string.trim() === '') {
|
|
return '';
|
|
}
|
|
|
|
let pre = '';
|
|
let ret = '';
|
|
let escapeCode;
|
|
|
|
const lengths = wordLengths(string);
|
|
let rows = [''];
|
|
|
|
for (const [index, word] of string.split(' ').entries()) {
|
|
if (options.trim !== false) {
|
|
rows[rows.length - 1] = rows[rows.length - 1].trimLeft();
|
|
}
|
|
|
|
let rowLength = stringWidth(rows[rows.length - 1]);
|
|
|
|
if (index !== 0) {
|
|
if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
|
|
// If we start with a new word but the current row length equals the length of the columns, add a new row
|
|
rows.push('');
|
|
rowLength = 0;
|
|
}
|
|
|
|
if (rowLength > 0 || options.trim === false) {
|
|
rows[rows.length - 1] += ' ';
|
|
rowLength++;
|
|
}
|
|
}
|
|
|
|
// In 'hard' wrap mode, the length of a line is
|
|
// never allowed to extend past 'columns'
|
|
if (options.hard && lengths[index] > columns) {
|
|
const remainingColumns = (columns - rowLength);
|
|
const breaksStartingThisLine = 1 + Math.floor((lengths[index] - remainingColumns - 1) / columns);
|
|
const breaksStartingNextLine = Math.floor((lengths[index] - 1) / columns);
|
|
if (breaksStartingNextLine < breaksStartingThisLine) {
|
|
rows.push('');
|
|
}
|
|
|
|
wrapWord(rows, word, columns);
|
|
continue;
|
|
}
|
|
|
|
if (rowLength + lengths[index] > columns && rowLength > 0 && lengths[index] > 0) {
|
|
if (options.wordWrap === false && rowLength < columns) {
|
|
wrapWord(rows, word, columns);
|
|
continue;
|
|
}
|
|
|
|
rows.push('');
|
|
}
|
|
|
|
if (rowLength + lengths[index] > columns && options.wordWrap === false) {
|
|
wrapWord(rows, word, columns);
|
|
continue;
|
|
}
|
|
|
|
rows[rows.length - 1] += word;
|
|
}
|
|
|
|
if (options.trim !== false) {
|
|
rows = rows.map(stringVisibleTrimSpacesRight);
|
|
}
|
|
|
|
pre = rows.join('\n');
|
|
|
|
for (const [index, character] of [...pre].entries()) {
|
|
ret += character;
|
|
|
|
if (ESCAPES.has(character)) {
|
|
const code = parseFloat(/\d[^m]*/.exec(pre.slice(index, index + 4)));
|
|
escapeCode = code === END_CODE ? null : code;
|
|
}
|
|
|
|
const code = ansiStyles.codes.get(Number(escapeCode));
|
|
|
|
if (escapeCode && code) {
|
|
if (pre[index + 1] === '\n') {
|
|
ret += wrapAnsi(code);
|
|
} else if (character === '\n') {
|
|
ret += wrapAnsi(escapeCode);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
// For each newline, invoke the method separately
|
|
module.exports = (string, columns, options) => {
|
|
return String(string)
|
|
.normalize()
|
|
.split('\n')
|
|
.map(line => exec(line, columns, options))
|
|
.join('\n');
|
|
};
|