UNPKG

tty-strings

Version:

Tools for working with strings displayed in the terminal

246 lines 12.6 kB
"use strict"; 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