@visulima/string
Version:
Functions for manipulating strings.
546 lines (538 loc) • 16.8 kB
JavaScript
import { P as ESCAPES, Q as ANSI_ESCAPE_LINK, S as ANSI_SGR_TERMINATOR, T as ANSI_ESCAPE_BELL, U as ANSI_CSI, V as RE_ESCAPE_PATTERN, W as ANSI_RESET_CODES, X as END_CODE, Y as RE_ZERO_WIDTH } from './constants-CKNmLDBQ.mjs';
import { getStringWidth } from '../get-string-width.mjs';
var __defProp$5 = Object.defineProperty;
var __name$5 = (target, value) => __defProp$5(target, "name", { value, configurable: true });
class AnsiStateTracker {
static {
__name$5(this, "AnsiStateTracker");
}
activeForeground = null;
activeBackground = null;
// Track other formatting (bold, italic, etc.)
activeFormatting = [];
/**
* Processes an escape sequence and updates the internal state
* @param sequence - The escape sequence to process
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
processEscape(sequence) {
const match = /\x1B\[(\d+)m/.exec(sequence);
if (!match) {
return;
}
const code = Number.parseInt(match[1], 10);
switch (code) {
case 0: {
this.activeForeground = null;
this.activeBackground = null;
this.activeFormatting = [];
break;
}
case 39: {
this.activeForeground = null;
break;
}
case 49: {
this.activeBackground = null;
break;
}
default: {
if (code >= 30 && code <= 37 || code >= 90 && code <= 97) {
this.activeForeground = sequence;
} else if (code >= 40 && code <= 47 || code >= 100 && code <= 107) {
this.activeBackground = sequence;
} else if ([1, 2, 3, 4, 7, 8, 9].includes(code)) {
this.activeFormatting.push(sequence);
} else if ([22, 23, 24, 27, 28, 29].includes(code)) {
const formatResetMap = {
22: "[1m",
// Reset bold
23: "[3m",
// Reset italic
24: "[4m",
// Reset underline
27: "[7m",
// Reset inverse
28: "[8m",
// Reset hidden
29: "[9m"
// Reset strikethrough
};
const formatToRemove = formatResetMap[code];
if (formatToRemove) {
this.activeFormatting = this.activeFormatting.filter((fmt) => !fmt.includes(formatToRemove));
}
}
}
}
}
/**
* Gets all active escape sequences to apply
* @returns String with all active escapes
*/
getActiveEscapes() {
return [this.activeBackground, this.activeForeground, ...this.activeFormatting].filter(Boolean).join("");
}
}
var __defProp$4 = Object.defineProperty;
var __name$4 = (target, value) => __defProp$4(target, "name", { value, configurable: true });
const checkEscapeSequence = /* @__PURE__ */ __name$4((chars, index) => {
if (!ESCAPES.has(chars[index])) {
return { isInsideEscape: false, isInsideLinkEscape: false };
}
const isInsideEscape = true;
const possibleLink = chars.slice(index + 1, index + 1 + ANSI_ESCAPE_LINK.length).join("");
const isInsideLinkEscape = possibleLink === ANSI_ESCAPE_LINK;
return { isInsideEscape, isInsideLinkEscape };
}, "checkEscapeSequence");
const processAnsiString = /* @__PURE__ */ __name$4((string, options = {}) => {
const stateTracker = new AnsiStateTracker();
let currentText = "";
let isInsideEscape = false;
let isInsideLinkEscape = false;
let escapeBuffer = "";
let currentUrl = "";
let isInHyperlink = false;
const chars = [...string];
for (let index = 0; index < chars.length; index++) {
const character = chars[index];
if (character && ESCAPES.has(character)) {
if (currentText) {
const width2 = options.getWidth?.(currentText) ?? 0;
const segment2 = {
isEscapeSequence: false,
isGrapheme: true,
text: currentText,
width: width2
};
if (isInHyperlink) {
segment2.isHyperlink = true;
segment2.hyperlinkUrl = currentUrl;
}
if (options.onSegment?.(segment2, stateTracker) === false) {
return;
}
currentText = "";
}
isInsideEscape = true;
escapeBuffer = character;
const escapeInfo = checkEscapeSequence(chars, index);
isInsideLinkEscape = escapeInfo.isInsideLinkEscape;
if (isInsideLinkEscape) {
let urlEnd = index + 1;
currentUrl = "";
while (urlEnd < chars.length) {
const nextChar = chars[urlEnd];
if (nextChar === ANSI_ESCAPE_BELL) {
break;
}
currentUrl += nextChar;
urlEnd++;
}
currentUrl = currentUrl.slice(4);
const segment2 = {
hyperlinkUrl: currentUrl,
isEscapeSequence: true,
isGrapheme: false,
isHyperlink: true,
isHyperlinkStart: true,
width: 0
};
if (options.onSegment?.(segment2, stateTracker) === false) {
return;
}
index = urlEnd;
isInHyperlink = true;
isInsideEscape = false;
isInsideLinkEscape = false;
escapeBuffer = "";
continue;
}
if (index + 1 < chars.length && chars[index + 1] === "\\" && isInHyperlink) {
const segment2 = {
isEscapeSequence: true,
isGrapheme: false,
isHyperlink: true,
isHyperlinkEnd: true,
width: 0
};
if (options.onSegment?.(segment2, stateTracker) === false) {
return;
}
isInHyperlink = false;
currentUrl = "";
index++;
isInsideEscape = false;
escapeBuffer = "";
continue;
}
}
if (isInsideEscape) {
if (escapeBuffer !== character) {
escapeBuffer += character;
}
if (character === ANSI_SGR_TERMINATOR) {
isInsideEscape = false;
stateTracker.processEscape(escapeBuffer);
const segment2 = {
isEscapeSequence: true,
isGrapheme: false,
text: escapeBuffer,
width: 0
};
if (options.onSegment?.(segment2, stateTracker) === false) {
return;
}
escapeBuffer = "";
}
continue;
}
currentText += character;
const width = options.getWidth?.(currentText) ?? 0;
const segment = {
isEscapeSequence: false,
isGrapheme: true,
text: currentText,
width
};
if (isInHyperlink) {
segment.isHyperlink = true;
segment.hyperlinkUrl = currentUrl;
}
if (options.onSegment?.(segment, stateTracker) === false) {
return;
}
currentText = "";
}
if (currentText) {
const width = options.getWidth?.(currentText) ?? 0;
const segment = {
isEscapeSequence: false,
isGrapheme: true,
text: currentText,
width
};
if (isInHyperlink) {
segment.isHyperlink = true;
segment.hyperlinkUrl = currentUrl;
}
options.onSegment?.(segment, stateTracker);
}
if (escapeBuffer) {
const segment = {
isEscapeSequence: true,
isGrapheme: false,
text: escapeBuffer,
width: 0
};
options.onSegment?.(segment, stateTracker);
}
}, "processAnsiString");
var __defProp$3 = Object.defineProperty;
var __name$3 = (target, value) => __defProp$3(target, "name", { value, configurable: true });
const wrapAnsiCode = /* @__PURE__ */ __name$3((code) => {
const escapeChar = ESCAPES.values().next().value;
return `${escapeChar}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
}, "wrapAnsiCode");
var __defProp$2 = Object.defineProperty;
var __name$2 = (target, value) => __defProp$2(target, "name", { value, configurable: true });
const wrapAnsiHyperlink = /* @__PURE__ */ __name$2((url) => {
const escapeChar = ESCAPES.values().next().value;
return `${escapeChar}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
}, "wrapAnsiHyperlink");
var __defProp$1 = Object.defineProperty;
var __name$1 = (target, value) => __defProp$1(target, "name", { value, configurable: true });
const preserveAnsi = /* @__PURE__ */ __name$1((rawLines) => {
if (rawLines.length === 0) {
return "";
}
if (rawLines.length === 1) {
return rawLines[0];
}
let returnValue = "";
let escapeCode;
let escapeUrl;
const preString = rawLines.join("\n");
const pre = [...preString];
let preStringIndex = 0;
for (const [index, character] of pre.entries()) {
returnValue += character;
if (ESCAPES.has(character)) {
const match = RE_ESCAPE_PATTERN.exec(preString.slice(preStringIndex));
const groups = match?.groups ?? {};
if (groups.code !== void 0) {
const code2 = Number.parseFloat(groups.code);
escapeCode = code2 === END_CODE ? void 0 : code2;
} else if (groups.uri !== void 0) {
escapeUrl = groups.uri.length === 0 ? void 0 : groups.uri;
}
}
const code = ANSI_RESET_CODES.get(Number(escapeCode));
if (pre[index + 1] === "\n") {
if (escapeUrl) {
returnValue += wrapAnsiHyperlink("");
}
if (escapeCode && code) {
returnValue += wrapAnsiCode(code);
}
} else if (character === "\n") {
if (escapeCode && code) {
returnValue += wrapAnsiCode(escapeCode);
}
if (escapeUrl) {
returnValue += wrapAnsiHyperlink(escapeUrl);
}
}
preStringIndex += character.length;
}
return returnValue;
}, "preserveAnsi");
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
const resetAnsiAtLineBreak = /* @__PURE__ */ __name((currentLine) => {
if (!currentLine.includes("\x1B")) {
return currentLine;
}
let result = currentLine;
if (currentLine.includes("\x1B[30m")) {
result += "\x1B[39m";
}
if (currentLine.includes("\x1B[42m")) {
result += "\x1B[49m";
}
return result;
}, "resetAnsiAtLineBreak");
const stringVisibleTrimSpacesRight = /* @__PURE__ */ __name((string) => {
const words = string.split(" ");
let last = words.length;
while (last > 0 && getStringWidth(words[last - 1]) === 0) {
last--;
}
if (last === words.length) {
return string;
}
return words.slice(0, last).join(" ") + words.slice(last).join("");
}, "stringVisibleTrimSpacesRight");
const wrapWithBreakAtWidth = /* @__PURE__ */ __name((string, width, trim) => {
if (string.length === 0) {
return [""];
}
if (width <= 0) {
return [string];
}
const rows = [];
let currentLine = "";
let currentWidth = 0;
const ansiTracker = new AnsiStateTracker();
let isInsideEscape = false;
let isInsideLinkEscape = false;
let escapeBuffer = "";
for (let index = 0; index < string.length; index++) {
const char = string[index];
if (ESCAPES.has(char)) {
isInsideEscape = true;
escapeBuffer = char;
currentLine += char;
const escapeInfo = checkEscapeSequence([...string], index);
isInsideLinkEscape = escapeInfo.isInsideLinkEscape;
continue;
}
if (isInsideEscape) {
escapeBuffer += char;
currentLine += char;
if (isInsideLinkEscape) {
if (char === ANSI_ESCAPE_BELL) {
isInsideEscape = isInsideLinkEscape = false;
}
} else if (char === ANSI_SGR_TERMINATOR) {
isInsideEscape = false;
ansiTracker.processEscape(escapeBuffer);
}
continue;
}
const charWidth = getStringWidth(char);
const isSpace = char === " ";
if (charWidth === 0) {
currentLine += char;
continue;
}
if (currentWidth + charWidth > width) {
if (currentLine) {
rows.push(resetAnsiAtLineBreak(currentLine));
}
currentLine = ansiTracker.getActiveEscapes();
currentWidth = 0;
if (isSpace && trim) {
while (index < string.length && string[index] === " ") {
index++;
if (index >= string.length) {
break;
}
}
if (index < string.length) {
index--;
}
continue;
}
}
currentLine += char;
currentWidth += charWidth;
if (currentWidth === width && index < string.length - 1) {
rows.push(resetAnsiAtLineBreak(currentLine));
currentLine = ansiTracker.getActiveEscapes();
currentWidth = 0;
if (index + 1 < string.length && string[index + 1] === " " && trim) {
index++;
while (index < string.length && string[index] === " ") {
index++;
}
index--;
}
}
}
if (currentLine) {
rows.push(currentLine);
}
return trim ? rows.map((element) => stringVisibleTrimSpacesRight(element)) : rows;
}, "wrapWithBreakAtWidth");
const wrapCharByChar = /* @__PURE__ */ __name((string, width, trim) => {
if (string.length === 0) {
return [];
}
const inputToProcess = trim ? string.trim() : string;
if (inputToProcess.length === 0) {
return [];
}
const rows = [];
let currentLine = "";
let currentWidth = 0;
processAnsiString(inputToProcess, {
getWidth: getStringWidth,
onSegment: /* @__PURE__ */ __name((segment, stateTracker) => {
if (segment.isEscapeSequence) {
currentLine += segment.text;
} else {
const isSpace = segment.text === " ";
if (segment.width === 0) {
currentLine += segment.text;
return true;
}
if (currentWidth + segment.width > width) {
if (currentLine) {
rows.push(resetAnsiAtLineBreak(currentLine));
}
currentLine = stateTracker.getActiveEscapes();
currentWidth = 0;
if (isSpace) {
if (trim) {
return true;
}
rows.push(stateTracker.getActiveEscapes() + segment.text);
return true;
}
}
currentLine += segment.text;
currentWidth += segment.width;
}
return true;
}, "onSegment")
});
if (currentLine) {
rows.push(currentLine);
}
return trim ? rows.map((row) => stringVisibleTrimSpacesRight(row)) : rows;
}, "wrapCharByChar");
const wrapWithWordBoundaries = /* @__PURE__ */ __name((string, width, trim) => {
if (string.length === 0) {
return [];
}
const inputToProcess = trim ? string.trim() : string;
if (inputToProcess.length === 0) {
return [];
}
const tokens = inputToProcess.split(/(?=\s)|(?<=\s)/);
const rows = [];
let currentLine = "";
let currentWidth = 0;
let index = 0;
while (index < tokens.length) {
const token = tokens[index];
const isSpace = /^\s+$/.test(token);
const tokenVisibleWidth = getStringWidth(token);
if (token.length === 0) {
index++;
continue;
}
if (trim && isSpace && currentWidth === 0) {
index++;
continue;
}
if (currentWidth + tokenVisibleWidth > width && currentWidth > 0) {
if (trim) {
rows.push(stringVisibleTrimSpacesRight(currentLine));
} else {
rows.push(currentLine);
}
currentLine = "";
currentWidth = 0;
continue;
}
currentLine += token;
currentWidth += tokenVisibleWidth;
index++;
}
if (currentLine) {
if (trim) {
rows.push(stringVisibleTrimSpacesRight(currentLine));
} else {
rows.push(currentLine);
}
}
return rows;
}, "wrapWithWordBoundaries");
const WrapMode = {
/**
* Breaks words at character boundaries to fit the width
*/
BREAK_AT_CHARACTERS: "BREAK_AT_CHARACTERS",
/**
* Preserves word boundaries, words are kept intact even if they exceed width
*/
PRESERVE_WORDS: "PRESERVE_WORDS",
/**
* Enforces strict adherence to the width limit by breaking at exact width
*/
STRICT_WIDTH: "STRICT_WIDTH"
};
const wordWrap = /* @__PURE__ */ __name((string, options = {}) => {
const { removeZeroWidthCharacters = true, trim = true, width = 80, wrapMode = WrapMode.PRESERVE_WORDS } = options;
if (trim && string.trim() === "") {
return "";
}
let normalizedString = String(string).normalize("NFC").replaceAll("\r\n", "\n");
if (removeZeroWidthCharacters) {
normalizedString = normalizedString.replaceAll(RE_ZERO_WIDTH, "");
}
const result = normalizedString.split("\n").map((line) => {
if (trim && line.trim() === "") {
return "";
}
let wrappedLines;
switch (wrapMode) {
case WrapMode.STRICT_WIDTH: {
wrappedLines = wrapWithBreakAtWidth(line, width, trim);
break;
}
case WrapMode.BREAK_AT_CHARACTERS: {
wrappedLines = wrapCharByChar(line, width, trim);
break;
}
default: {
wrappedLines = wrapWithWordBoundaries(line, width, trim);
}
}
return preserveAnsi(wrappedLines);
});
return result.join("\n");
}, "wordWrap");
export { WrapMode, wordWrap };