slice-ansi
Version:
Slice a string with ANSI escape codes
168 lines (137 loc) • 3.88 kB
JavaScript
import ansiStyles from 'ansi-styles';
import isFullwidthCodePoint from 'is-fullwidth-code-point';
// \x1b and \x9b
const ESCAPES = new Set([27, 155]);
const CODE_POINT_0 = '0'.codePointAt(0);
const CODE_POINT_9 = '9'.codePointAt(0);
const endCodesSet = new Set();
const endCodesMap = new Map();
for (const [start, end] of ansiStyles.codes) {
endCodesSet.add(ansiStyles.color.ansi(end));
endCodesMap.set(ansiStyles.color.ansi(start), ansiStyles.color.ansi(end));
}
function getEndCode(code) {
if (endCodesSet.has(code)) {
return code;
}
if (endCodesMap.has(code)) {
return endCodesMap.get(code);
}
code = code.slice(2);
if (code.includes(';')) {
code = code[0] + '0';
}
const returnValue = ansiStyles.codes.get(Number.parseInt(code, 10));
if (returnValue) {
return ansiStyles.color.ansi(returnValue);
}
return ansiStyles.reset.open;
}
function findNumberIndex(string) {
for (let index = 0; index < string.length; index++) {
const codePoint = string.codePointAt(index);
if (codePoint >= CODE_POINT_0 && codePoint <= CODE_POINT_9) {
return index;
}
}
return -1;
}
function parseAnsiCode(string, offset) {
string = string.slice(offset, offset + 19);
const startIndex = findNumberIndex(string);
if (startIndex !== -1) {
let endIndex = string.indexOf('m', startIndex);
if (endIndex === -1) {
endIndex = string.length;
}
return string.slice(0, endIndex + 1);
}
}
function tokenize(string, endCharacter = Number.POSITIVE_INFINITY) {
const returnValue = [];
let index = 0;
let visibleCount = 0;
while (index < string.length) {
const codePoint = string.codePointAt(index);
if (ESCAPES.has(codePoint)) {
const code = parseAnsiCode(string, index);
if (code) {
returnValue.push({
type: 'ansi',
code,
endCode: getEndCode(code),
});
index += code.length;
continue;
}
}
const isFullWidth = isFullwidthCodePoint(codePoint);
const character = String.fromCodePoint(codePoint);
returnValue.push({
type: 'character',
value: character,
isFullWidth,
});
index += character.length;
visibleCount += isFullWidth ? 2 : character.length;
if (visibleCount >= endCharacter) {
break;
}
}
return returnValue;
}
function reduceAnsiCodes(codes) {
let returnValue = [];
for (const code of codes) {
if (code.code === ansiStyles.reset.open) {
// Reset code, disable all codes
returnValue = [];
} else if (endCodesSet.has(code.code)) {
// This is an end code, disable all matching start codes
returnValue = returnValue.filter(returnValueCode => returnValueCode.endCode !== code.code);
} else {
// This is a start code. Disable all styles this "overrides", then enable it
returnValue = returnValue.filter(returnValueCode => returnValueCode.endCode !== code.endCode);
returnValue.push(code);
}
}
return returnValue;
}
function undoAnsiCodes(codes) {
const reduced = reduceAnsiCodes(codes);
const endCodes = reduced.map(({endCode}) => endCode);
return endCodes.reverse().join('');
}
export default function sliceAnsi(string, start, end) {
const tokens = tokenize(string, end);
let activeCodes = [];
let position = 0;
let returnValue = '';
let include = false;
for (const token of tokens) {
if (end !== undefined && position >= end) {
break;
}
if (token.type === 'ansi') {
activeCodes.push(token);
if (include) {
returnValue += token.code;
}
} else {
// Character
if (!include && position >= start) {
include = true;
// Simplify active codes
activeCodes = reduceAnsiCodes(activeCodes);
returnValue = activeCodes.map(({code}) => code).join('');
}
if (include) {
returnValue += token.value;
}
position += token.isFullWidth ? 2 : token.value.length;
}
}
// Disable active codes at the end
returnValue += undoAnsiCodes(activeCodes);
return returnValue;
}