tty-strings
Version:
Tools for working with strings displayed in the terminal
251 lines • 10.2 kB
JavaScript
;
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