UNPKG

tty-strings

Version:

Tools for working with strings displayed in the terminal

251 lines 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.closeEscapes = exports.openEscapes = exports.parseEscape = exports.parseAnsi = void 0; const ansiRegex_1 = require("./ansiRegex"); const ansiCodes_1 = require("./ansiCodes"); /** * A generator function that matches all ansi escape sequences within a string, yielding sequential * chunks that are either a matched escape sequence or a run of plain text between matches. * * @param string - input string * @returns - A generator that yields `[chunk, isEscape]` pairs */ function* parseAnsi(string) { // lower index of the chunk preceeding each escape sequence let i = 0; // match all ansi escape codes for (let regex = (0, ansiRegex_1.default)(), m = regex.exec(string); m; m = regex.exec(string)) { const { 0: seq, index: j } = m; // yield the chunk preceeding this escape if its length > 0 if (j > i) yield [string.slice(i, j), false]; // yield the matched escape yield [seq, true]; // set lower string index of the next chunk to be processed i = j + seq.length; } // yield the final chunk of string if its length > 0 if (i < string.length) yield [string.slice(i), false]; } exports.parseAnsi = parseAnsi; /** * Parse an SGR parameter string into commands * @param str - SGR parameter string, consisting of numerical digits (0 - 9) and delimeters : & ; */ function* parseSgrParams(str) { // start by parsing ; separated commands let digitIsSub = false, current = { value: -1 }; const params = [current]; for (let i = 0; i < str.length; i += 1) { const code = str.charCodeAt(i); switch (code) { case 0x3b: // semicolon ; digitIsSub = false; current = { value: -1 }; params.push(current); break; case 0x3a: // colon : digitIsSub = true; if (current.subparams) current.subparams.push(-1); else current.subparams = [-1]; break; default: { // digit 0x30 - 0x39 const digit = code - 48; if (digitIsSub) { const sub = current.subparams, cur = sub[sub.length - 1]; sub[sub.length - 1] = ~cur ? cur * 10 + digit : digit; } else current.value = ~current.value ? current.value * 10 + digit : digit; break; } } } // group params into commands for (let i = 0; i < params.length; i += 1) { const param = params[i], // get command code. -1 is converted to 0 (ZDM) code = Math.max(param.value, 0); if (code === 38 || code === 48 || code === 58) { // stop if this params has subparams, or if there are no more params to consume if (param.subparams) { yield { code, params: param }; continue; } // store param starting index const start = i; // move to next param i += 1; // consume next param to get color model let next = params[i]; // determine how many params need to be consumed based on the color model (only 2 & 5 are supported) const args = (next === null || next === void 0 ? void 0 : next.value) === 2 ? 3 : (next === null || next === void 0 ? void 0 : next.value) === 5 ? 1 : 0; // consume params for (let n = 0; next && !next.subparams && n < args; n += 1, i += 1, next = params[i]) ; // determine param span length const len = Math.min(i + 1, params.length) - start; // yield this color command yield { code, params: len === 1 ? param : params.slice(start, start + len), incomplete: !(next === null || next === void 0 ? void 0 : next.subparams) && len < args + 2, }; continue; } yield { code, params: param }; } } /** * Stringify a parsed SGR param, preserving the original syntax */ function stringifySgrParam(param) { return param.subparams ? [param.value, ...param.subparams].map((v) => (~v ? String(v) : '')).join(':') : ~param.value ? String(param.value) : ''; } const sgrLinkRegex = /(?:(?:\x1b\x5b|\x9b)([\x30-\x3b]*m)|(?:\x1b\x5d|\x9d)8;(.*?)(?:\x1b\x5c|[\x07\x9c]))/; function parseEscape(stack, seq, idx) { var _a, _b; const [, sgr, link] = (_a = sgrLinkRegex.exec(seq)) !== null && _a !== void 0 ? _a : [], // array of indexes of stack items closed by this sequence closedIndex = []; // update ansi escape stack if (sgr) { // parse each sgr param for (const { code, params, incomplete } of parseSgrParams(sgr.slice(0, -1))) { let id = String(code); // if command is underline with subparams, make id '4:0' or '4:' if (code === 4 && params.subparams) id += params.subparams[0] === 0 ? ':0' : ':'; // check if this is a closing code if (ansiCodes_1.closingCodes.includes(id)) { // create alias list for the two different underline close escapes const aliases = (id === '4:0' || id === '24') ? ['4:0', '24'] : null; // remove all escapes that this sequence closes from the stack for (let x = stack.length - 1; x >= 0; x -= 1) { const [, attr, close] = stack[x]; // if item is a link or has already been closed within this sequence, skip it if (attr === 1 || closedIndex.includes(x)) continue; // if item not closed by this code, continue if (id !== '0' && (aliases ? !aliases.includes(close) : id !== close)) continue; // if closed by reset, update the stack item if (id === '0') stack[x][2] = '0'; // add stack item index to closed array closedIndex.push(x); } continue; } // stringify param to preserve original syntax const str = Array.isArray(params) ? params.map(stringifySgrParam).join(';') : stringifySgrParam(params); // add this ansi escape to the stack stack.push([str, incomplete ? 2 : 0, (_b = ansiCodes_1.styleCodes.get(id)) !== null && _b !== void 0 ? _b : '0', idx]); } } else if (link !== undefined) { // escape follows this pattern: OSC 8 ; [params] ; [url] ST, so params portion must be removed to get the url const url = link.replace(/^[^;]*;/, ''); // ignore malformed hyperlink escapes that do not contain an additional ; delimeter if (link === url) return null; // if url is empty, then this is a closing hyperlink sequence if (url === '') { // remove all hyperlink escapes from the stack for (let x = stack.length - 1; x >= 0; x -= 1) { const [, attr] = stack[x]; // if item is not an open hyperlink, skip it if (attr !== 1 || closedIndex.includes(x)) continue; // update the closing sequence for this link stack[x][2] = seq; // add stack item index to closed array closedIndex.push(x); } } else { // add this hyperlink escape to the stack const close = seq.replace(/((?:\x1b\x5d|\x9d)8;).*?(\x1b\x5c|[\x07\x9c])/, '$1;$2'); stack.push([seq, 1, close, idx]); } } else { // return null on a non SGR/hyperlink escape sequence return null; } const closed = []; // remove closed items from the stack if (closedIndex.length) { for (const x of closedIndex.sort((a, b) => b - a)) { // add stack item to closed array closed.unshift(stack[x]); // remove style sequence from the stack stack.splice(x, 1); } } return closed; } exports.parseEscape = parseEscape; function openEscapes(stack) { let esc = '', sgr = []; for (const [seq, attr] of stack) { // handle links if (attr === 1) { // add any accumulated sgr escapes if (sgr.length) { esc += `\x1b[${sgr.join(';')}m`; sgr = []; } esc += seq; continue; } // add sgr sequence to list of accumulated sgr escapes sgr.push(seq); // if this sequence is incomplete and will consume subsequent parameters, escapes must be broken up if (attr === 2) { esc += `\x1b[${sgr.join(';')}m`; sgr = []; } } return esc + (sgr.length ? `\x1b[${sgr.join(';')}m` : ''); } exports.openEscapes = openEscapes; function closeEscapes(stack) { const sgr = []; let link = ''; for (const [, attr, close] of stack) { if (attr === 1) { link = close; continue; } // don't add duplicate close commands if (sgr.includes(close)) continue; // add new underline close escape if old one is not present if (close === '4:0') { if (!sgr.includes('24')) sgr.unshift(close); continue; } // legacy underline close overrides newer 4:0 close if (close === '24') { const index = sgr.indexOf('4:0'); if (index < 0) sgr.unshift(close); else sgr[index] = close; continue; } // add any other close escape sgr.unshift(close); } return link + (sgr.length ? (sgr.includes('0') ? '\x1b[0m' : `\x1b[${sgr.join(';')}m`) : ''); } exports.closeEscapes = closeEscapes; //# sourceMappingURL=utils.js.map