Skip to content

Commit

Permalink
Handle slicing of links (chalk#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
dspasojevic committed Mar 22, 2021
1 parent 1d1eb94 commit 9cd0ec1
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 13 deletions.
90 changes: 78 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,41 @@ const ESCAPES = [
'\u009B'
];

const wrapAnsi = code => `${ESCAPES[0]}[${code}m`;
const ANSI_ESCAPE_BELL = '\u0007';
const ANSI_CSI = '[';
const ANSI_OSC = ']';
const ANSI_SGR_TERMINATOR = 'm';
const ANSI_SEP = ';';
const ANSI_ESCAPE_LINK = `${ANSI_OSC}8${ANSI_SEP}${ANSI_SEP}`;
const ANSI_LINK_TEXT_TERMINATOR = `${ESCAPES[0]}${ANSI_ESCAPE_LINK}${ANSI_ESCAPE_BELL}`;

const wrapAnsi = code => `${ESCAPES[0]}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
const wrapAnsiHyperlinkUri = uri => `${ESCAPES[0]}${ANSI_ESCAPE_LINK}${uri}${ANSI_ESCAPE_BELL}`;

const checkAnsi = (ansiCodes, isEscapes, endAnsiCode) => {
let output = [];
ansiCodes = [...ansiCodes];

for (let ansiCode of ansiCodes) {
const ansiCodeOrigin = ansiCode;
if (ansiCode.includes(';')) {
const ansiCodeOriginal = ansiCode;

if (ansiCode && ansiCode.includes(';')) {
ansiCode = ansiCode.split(';')[0][0] + '0';
}

const item = ansiStyles.codes.get(Number.parseInt(ansiCode, 10));
if (item) {
const indexEscape = ansiCodes.indexOf(item.toString());
if (indexEscape === -1) {
output.push(wrapAnsi(isEscapes ? item : ansiCodeOrigin));
output.push(wrapAnsi(isEscapes ? item : ansiCodeOriginal));
} else {
ansiCodes.splice(indexEscape, 1);
}
} else if (isEscapes) {
output.push(wrapAnsi(0));
break;
} else {
output.push(wrapAnsi(ansiCodeOrigin));
output.push(wrapAnsi(ansiCodeOriginal));
}
}

Expand All @@ -53,17 +63,41 @@ module.exports = (string, begin, end) => {
const ansiCodes = [];

let stringEnd = typeof end === 'number' ? end : characters.length;

// Track the state of the three types of escape code (regular, link uri, link text)
let isInsideEscape = false;
let isInsideLinkUriEscape = false;
let isInsideLinkTextEscape = false;

let ansiCode;

// How many visible characters have been added to the output. Characters added while isInsideEscape is true
// do not count towards this total.
let visible = 0;
let output = '';
let escapeUri;

// Has the URI been added, so we need to terminate the link if we get to the end without terminating somehow.
let needToTerminateLink = false;

// Whether we are processing the link text.
let processingLinkText = false;

for (const [index, character] of characters.entries()) {
let leftEscape = false;

if (ESCAPES.includes(character)) {
const code = /\d[^m]*/.exec(string.slice(index, index + 18));
ansiCode = code && code.length > 0 ? code[0] : undefined;
const remainingString = string.slice(index);
const {groups} = new RegExp(`(?:\\${ANSI_CSI}(?<code>\\d[^m]*)|\\${ANSI_ESCAPE_LINK}(?<uri>[^${ANSI_ESCAPE_BELL}]*)${ANSI_ESCAPE_BELL})`).exec(remainingString) || {groups: {}};
if (groups.code !== undefined) {
ansiCode = groups.code;
} else if (groups.uri !== undefined && !processingLinkText) {
escapeUri = groups.uri.length === 0 ? null : groups.uri;
isInsideLinkUriEscape = true;
} else if (remainingString.startsWith(ANSI_LINK_TEXT_TERMINATOR)) {
isInsideLinkTextEscape = true;
processingLinkText = false;
}

if (visible < stringEnd) {
isInsideEscape = true;
Expand All @@ -72,9 +106,27 @@ module.exports = (string, begin, end) => {
ansiCodes.push(ansiCode);
}
}
} else if (isInsideEscape && character === 'm') {
isInsideEscape = false;
leftEscape = true;
}

if (isInsideEscape) {
if (isInsideLinkUriEscape) {
if (character === ANSI_ESCAPE_BELL) {
isInsideEscape = false;
isInsideLinkUriEscape = false;
leftEscape = true;
processingLinkText = true;
}
} else if (isInsideLinkTextEscape) {
if (character === ANSI_ESCAPE_BELL) {
isInsideEscape = false;
isInsideLinkTextEscape = false;
leftEscape = true;
needToTerminateLink = false;
}
} else if (character === ANSI_SGR_TERMINATOR) {
isInsideEscape = false;
leftEscape = true;
}
}

if (!isInsideEscape && !leftEscape) {
Expand All @@ -91,10 +143,24 @@ module.exports = (string, begin, end) => {

if (visible > begin && visible <= stringEnd) {
output += character;
} else if (visible === begin && !isInsideEscape && ansiCode !== undefined) {
output = checkAnsi(ansiCodes);
} else if (visible === begin && !isInsideEscape) {
output = '';
if (ansiCode !== undefined) {
output += checkAnsi(ansiCodes);
}

if (escapeUri !== undefined) {
output += wrapAnsiHyperlinkUri(escapeUri);
escapeUri = undefined;
needToTerminateLink = true;
}
} else if (visible >= stringEnd) {
output += checkAnsi(ansiCodes, true, ansiCode);

if (needToTerminateLink) {
output += ANSI_LINK_TEXT_TERMINATOR;
}

break;
}
}
Expand Down
20 changes: 19 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,25 @@ test('does not lose fullwidth characters', t => {
t.is(sliceAnsi('古古test', 0), '古古test');
});

test.failing('slice links', t => {
test('slice links', t => {
const link = '\u001B]8;;https://google.com\u0007Google\u001B]8;;\u0007';
t.is(sliceAnsi(link, 0, 6), link);
});

test('slice links - shortening', t => {
const link = '\u001B]8;;https://google.com\u0007Google\u001B]8;;\u0007';
const expected = '\u001B]8;;https://google.com\u0007Goog\u001B]8;;\u0007';
t.is(JSON.stringify(sliceAnsi(link, 0, 4)), JSON.stringify(expected));
});

test('slice links - going over link', t => {
const link = '\u001B]8;;https://google.com\u0007Google\u001B]8;;\u0007 and some more text';
const expected = '\u001B]8;;https://google.com\u0007Google\u001B]8;;\u0007 and s';
t.is(JSON.stringify(sliceAnsi(link, 0, 12)), JSON.stringify(expected));
});

test('slice links mid text', t => {
const link = 'some entry text \u001B]8;;https://google.com\u0007Google\u001B]8;;\u0007 and some more text';
const expected = 'some entry text \u001B]8;;https://google.com\u0007Google\u001B]8;;\u0007 an';
t.is(JSON.stringify(sliceAnsi(link, 0, 25)), JSON.stringify(expected));
});

0 comments on commit 9cd0ec1

Please sign in to comment.