From 9cd0ec103291aca942d904f58a941c57afc8e4e4 Mon Sep 17 00:00:00 2001 From: Daniel Spasojevic Date: Mon, 22 Mar 2021 22:53:21 +1100 Subject: [PATCH] Handle slicing of links (#32) --- index.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++++-------- test.js | 20 ++++++++++++- 2 files changed, 97 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 72d37d9..fb3d364 100755 --- a/index.js +++ b/index.js @@ -8,15 +8,25 @@ 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'; } @@ -24,7 +34,7 @@ const checkAnsi = (ansiCodes, isEscapes, endAnsiCode) => { 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); } @@ -32,7 +42,7 @@ const checkAnsi = (ansiCodes, isEscapes, endAnsiCode) => { output.push(wrapAnsi(0)); break; } else { - output.push(wrapAnsi(ansiCodeOrigin)); + output.push(wrapAnsi(ansiCodeOriginal)); } } @@ -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}(?\\d[^m]*)|\\${ANSI_ESCAPE_LINK}(?[^${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; @@ -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) { @@ -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; } } diff --git a/test.js b/test.js index 76f8d7d..31e940f 100755 --- a/test.js +++ b/test.js @@ -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)); +});