tty-strings
Version:
Tools for working with strings displayed in the terminal
246 lines • 12.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("./utils");
const charWidths_1 = require("./charWidths");
const stringWidth_1 = require("./stringWidth");
function filterStack(ansiStack, ax, ay) {
return ansiStack.filter(([, , , [x, y]]) => (x < ax || (x === ax && y <= ay)));
}
function wrapLine(ansiStack, string, columns, hard, ltrim) {
if (string.trim() === '')
return '';
// array to store each wrapped row as it is processed
const rows = [];
// the current row
let row = '',
// the current row length
rl = 0,
// word index of the active point in the ansi stack
ax = -Infinity,
// intraword index of the active point in the ansi stack
ay = 0,
// preserved space
space = '',
// index of the preserved spaces' active point in the ansi stack
sx = -Infinity,
// trimmed space
trimmed = '';
// split the input string into words and measure each one
const words = string.split(' ').map((word) => [word, (0, stringWidth_1.default)(word)]);
// loop through each word
for (const [ix, [word, len]] of words.entries()) {
// if this is not the first word
if (ix > 0) {
// increment the length of the current row if a space must be added before this word
if (rl > 0 || (!ltrim && ax < 0))
rl += 1;
// update preserved space with any new escapes found up through the end of the previous word
space += (0, utils_1.openEscapes)(ansiStack.filter(([, , , [x, y]]) => (x > sx || (x === sx && y > ay))));
// set the preserved space's active point in the ansi stack to the end of the last word
sx = ix;
// update accumulated space
space += ' ';
}
// if hard wrap mode is enabled, words whose length exceeds `columns` must be wrapped across multiple rows
if (hard && (len > columns || (!ltrim && ax < 0 && rl + len > columns && rl < columns))) {
// determine if a linebreak should occur before this hard wrapped word (adapted from `wrap-ansi`)
const rc = (columns - rl),
// compare the row span if hard wrapped word starts on the next line vs the current line
brkNext = Math.floor((len - 1) / columns) < (1 + Math.floor((len - rc - 1) / columns));
// check if word should break starting on the next row
if (brkNext && !(!ltrim && ax < 0)) {
// finalize the current row
rows.push(row + trimmed + (0, utils_1.closeEscapes)(filterStack(ansiStack, ax, ay)));
// start a new row
[rl, row] = [0, ''];
// clear preserved & trimmed space
[space, trimmed] = ['', ''];
}
// location within the current word
let iy = 0;
// match all ansi escape codes in this word
for (const [chunk, isEscape] of (0, utils_1.parseAnsi)(word)) {
// check if chunk is an escape sequence
if (isEscape) {
// parse the matched ansi escape sequence
const closed = (0, utils_1.parseEscape)(ansiStack, chunk, [ix, iy]);
if (closed === null || closed === void 0 ? void 0 : closed.length) {
if (ax < ix) {
// filter for active escapes in the current row
trimmed += (0, utils_1.closeEscapes)(filterStack(closed, ax, ay));
// filter for active escapes in the current row or preserved space
space += (0, utils_1.closeEscapes)(closed.filter(([, , , [cx]]) => cx < sx));
}
else if (rl) {
// filter for currently active escapes
row += (0, utils_1.closeEscapes)(filterStack(closed, ax, ay));
}
}
continue;
}
// store our current location within this word at the outset of this chunk
const sy = iy;
// see if the word index of the active point in the ansi stack needs to be updated
if (ax < ix) {
// check for edge case where large leading whitespace forces a line break before the first word
if (!ltrim && ax < 0 && rl === columns) {
// complete the current row of preserved leading whitespace
rows.push(row + space + (0, utils_1.closeEscapes)(filterStack(ansiStack, ix, -1)));
// start a new row
[rl, row] = [0, ''];
}
row += rl
// non-empty row - append preserved space & any escape sequences opened within this word
? space + (0, utils_1.openEscapes)(ansiStack.filter(([, , , [x]]) => x === ix))
// row is empty - initialize it with all active escape sequences
: (0, utils_1.openEscapes)(ansiStack);
// set the active point in the ansi stack to the beginning of the current word
[ax, ay] = [ix, 0];
// clear preserved space
[sx, space] = [ix, ''];
// clear trimmed space
trimmed = '';
}
// loop through the characters in this chunk
for (const [char, w] of (0, charWidths_1.default)(chunk)) {
// check if a line break is needed
if (rl + w > columns) {
// finalize the current row
rows.push(row + (0, utils_1.closeEscapes)(filterStack(ansiStack, ax, ay)));
// initialize a new row with all currently active escape sequences
[rl, row] = [0, (0, utils_1.openEscapes)(ansiStack)];
}
// see if the intra-word index of the active point in the ansi stack needs to be updated
if (ay < sy) {
// if row is not empty, add all new escape sequences from the ansi stack
if (rl)
row += (0, utils_1.openEscapes)(ansiStack.filter(([, , , [x, y]]) => (x === ax && ay < y)));
// update the active point in the ansi stack to the beginning of the current chunk
ay = sy;
}
// add this character to the current row & update the row length
row += char;
rl += w;
// increment current location within this word
iy += w;
}
}
continue;
}
// index location within the current word
let iy = 0;
// match all ansi escape codes in this word
for (const [chunk, isEscape] of (0, utils_1.parseAnsi)(word)) {
// check if chunk is an escape sequence
if (isEscape) {
// parse the matched ansi escape sequence
const closed = (0, utils_1.parseEscape)(ansiStack, chunk, [ix, iy]);
if (closed === null || closed === void 0 ? void 0 : closed.length) {
if (ax < ix) {
// filter for active escapes in the current row
trimmed += (0, utils_1.closeEscapes)(filterStack(closed, ax, ay));
// filter for active escapes in the current row or preserved space
space += (0, utils_1.closeEscapes)(closed.filter(([, , , [cx]]) => cx < sx));
}
else if (rl) {
// filter for currently active escapes
row += (0, utils_1.closeEscapes)(filterStack(closed, ax, ay));
}
}
continue;
}
// see if the word index of the active point in the ansi stack needs to be updated
if (ax < ix) {
// check if there should be a line break before this word
if ((!ltrim && ax < 0) ? (rl === columns) : (rl + len > columns && rl > 0)) {
// finalize the current row
rows.push((!ltrim && ax < 0)
// row of preserved leading whitespace
? row + space + (0, utils_1.closeEscapes)(filterStack(ansiStack, ix, -1))
// normal row
: row + trimmed + (0, utils_1.closeEscapes)(filterStack(ansiStack, ax, ay)));
// start a new row
[rl, row] = [0, ''];
}
row += rl
// non-empty row - append preserved space & any escape sequences opened within this word
? space + (0, utils_1.openEscapes)(ansiStack.filter(([, , , [x]]) => x === ix))
// row is empty - initialize it with all active escape sequences
: (0, utils_1.openEscapes)(ansiStack);
// add the length of this word to the current row length
rl += len;
// set the active point in the ansi stack to the beginning of the current word
[ax, ay] = [ix, 0];
// clear preserved space
[sx, space] = [ix, ''];
// clear trimmed space
trimmed = '';
}
// see if the intra-word index of the active point in the ansi stack needs to be updated
if (ay < iy) {
// add any new opening escapes to the current row (rl will always be > 0 here)
row += (0, utils_1.openEscapes)(ansiStack.filter(([, , , [x, y]]) => (x === ax && ay < y)));
// update the active point in the ansi stack to the beginning of the current chunk
ay = iy;
}
// add this chunk to the current row
row += chunk;
// update current location within this word
iy += chunk.length;
}
// check for edge case where large leading whitespace forces a line break before the first word
if (!ltrim && ax < 0 && rl === columns) {
// complete the current row of preserved leading whitespace
rows.push(row + space + (0, utils_1.closeEscapes)(filterStack(ansiStack, ix, -1)));
// start a new row
[rl, row] = [0, ''];
// reset space with all currently active escapes prior to `sx`
space = (0, utils_1.openEscapes)(filterStack(ansiStack, sx, ay));
// clear trimmed space
trimmed = '';
}
}
// finalize the last row if necessary
if (rl > 0)
rows.push(row + trimmed + (0, utils_1.closeEscapes)(filterStack(ansiStack, ax, ay)));
// update the indexes of any escape sequences that remain in the stack
for (const esc of ansiStack)
esc[3] = [-1, 0];
// return wrapped rows
return ax >= 0 ? rows.join('\n') : '';
}
/**
* Word wrap text to a specified column width.
*
* @remarks
* Input string may contain ANSI escape codes. Style and hyperlink sequences will be wrapped, while all
* other types of control sequences will be ignored and will not be included in the output string.
*
* @example
* ```ts
* import { wordWrap } from 'tty-strings';
* import chalk from 'chalk';
*
* const text = 'The ' + chalk.bgGreen.magenta('quick brown 🦊 jumps over') + ' the 😴 🐶.';
* console.log(wordWrap(text, 20));
* ```
*
* @param string - Input text to word wrap.
* @param columns - Column width to wrap text to.
* @param options - Word wrap options object.
* @returns The word wrapped string.
*/
function wordWrap(string, columns, { hard = false, trimLeft = true, } = {}) {
// initialize an ansi escapes stack, items are in the form [seq, isLink, close, [ix, iy]]
const ansiStack = [];
// ensure input is a string type
return String(string)
// split into lines
.split(/\r?\n/g)
// wrap each line
.map((line) => wrapLine(ansiStack, line, columns, hard, trimLeft))
// rejoin each wrapped line
.join('\n');
}
exports.default = wordWrap;
//# sourceMappingURL=wordWrap.js.map
;