-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
60cdc20
commit 0b58d48
Showing
3 changed files
with
209 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
const parseAnsi = require('./parseAnsi'), | ||
{ parseEscape } = require('./utils'), | ||
splitChars = require('./splitChars'), | ||
stringLength = require('./stringLength'); | ||
|
||
/** | ||
* Insert, remove or replace characters from a string, similar to the native `Array.splice()` method. | ||
* String may contain ANSI escape codes; inserted content will adopt any ANSI styling applied to the character | ||
* immediately preceding the insert point. | ||
* @param {string} string - input string from which to remove, insert, or replace characters | ||
* @param {number} start - character index at which to begin splicing | ||
* @param {number} deleteCount - number of characters to remove from the string | ||
* @param {string} [insert=''] - optional string to be inserted at the index specified by the `start` parameter | ||
* @returns {string} - the modified input string | ||
*/ | ||
module.exports = function spliceChars(string, start, deleteCount, insert = '') { | ||
// splice start index | ||
const startIndex = Math.max(start < 0 ? start + stringLength(string) : start, 0), | ||
// splice end index | ||
endIndex = startIndex + deleteCount, | ||
// ansi escapes stack, items in the form [seq, isLink, close, [idx, afterStart]] | ||
ansiStack = []; | ||
// the result string | ||
let result = '', | ||
// character index | ||
idx = 0, | ||
// conventional string index | ||
i = 0, | ||
// start found flag | ||
startFound = false, | ||
// end found flag | ||
endFound = false; | ||
// match all ansi escape codes | ||
for (const [chunk, isEscape] of parseAnsi(String(string))) { | ||
// check if chunk is an escape sequence | ||
if (isEscape) { | ||
// process this escape sequence | ||
const closed = parseEscape(ansiStack, chunk, [idx, startFound]); | ||
// check if insert point has been reached or if chunk is not a SGR/hyperlink escape | ||
if (closed === null || (!startFound && (idx < startIndex || insert))) { | ||
result += chunk; | ||
} else if (closed != null) { | ||
// append escape if it closes an active escape | ||
const [x, afterStart] = closed; | ||
if (!afterStart && (x < startIndex || insert)) result += chunk; | ||
} | ||
// increment conventional string index | ||
i += chunk.length; | ||
continue; | ||
} | ||
// check if the tail of the last chunk hit the end index | ||
if (endFound) { | ||
return result | ||
// append any open escapes found after the insert point | ||
+ ansiStack | ||
.filter(([,,, [x, afterStart]]) => afterStart || (x === startIndex && !insert)) | ||
.map(([s]) => s) | ||
.join('') | ||
// add the rest of the string | ||
+ string.slice(i); | ||
} | ||
// iterate through the characters in this chunk | ||
for (const char of splitChars(chunk)) { | ||
if (idx < startIndex) { | ||
result += char; | ||
} else { | ||
// check for the start of the splice | ||
if (idx === startIndex && !startFound) { | ||
result += insert; | ||
startFound = true; | ||
} | ||
// check for end of the splice | ||
if (idx === endIndex) { | ||
return result | ||
// append any open escapes found after the insert point | ||
+ ansiStack | ||
.filter(([,,, [x, afterStart]]) => afterStart || (x === startIndex && !insert)) | ||
.map(([s]) => s) | ||
.join('') | ||
// add the rest of the string | ||
+ string.slice(i); | ||
} | ||
} | ||
// increment char index | ||
idx += 1; | ||
// increment conventional string index | ||
i += char.length; | ||
} | ||
// check if at the end of this chunk, we have hit the start index | ||
if (idx === startIndex && !startFound) { | ||
result += insert; | ||
startFound = true; | ||
} | ||
// the end of this chunk hits the endIndex, set the `endFound` flag | ||
if (idx === endIndex) endFound = true; | ||
} | ||
return startFound ? result : result + insert; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
const chalk = require('chalk'), | ||
{ spliceChars } = require('..'); | ||
|
||
describe('spliceChars', () => { | ||
test('negative splice index', () => { | ||
expect(spliceChars('abef', -2, 0, 'CD')).toBe('abCDef'); | ||
expect(spliceChars(chalk.green('abef'), -2, 0, 'CD')).toBe(chalk.green('abCDef')); | ||
}); | ||
|
||
test('negative splice index exceeds the length of the string', () => { | ||
expect(spliceChars('def', -4, 0, 'ABC')).toBe('ABCdef'); | ||
expect(spliceChars(chalk.green('def'), -4, 0, 'ABC')).toBe(chalk.green('ABCdef')); | ||
}); | ||
|
||
test('splice index exceeds than the length of the string', () => { | ||
expect(spliceChars('abc', 4, 0, 'DEF')).toBe('abcDEF'); | ||
expect(spliceChars(chalk.green('abc'), 4, 0, 'DEF')).toBe(`${chalk.green('abc')}DEF`); | ||
}); | ||
|
||
describe('inserting characters', () => { | ||
test('at the beginning of a string', () => { | ||
expect(spliceChars('cdef', 0, 0, 'AB')).toBe('ABcdef'); | ||
expect(spliceChars(chalk.green('cdef'), 0, 0, 'AB')).toBe(chalk.green('ABcdef')); | ||
}); | ||
|
||
test('into the middle of a string', () => { | ||
expect(spliceChars('abef', 2, 0, 'CD')).toBe('abCDef'); | ||
expect(spliceChars(chalk.green('abef'), 2, 0, 'CD')).toBe(chalk.green('abCDef')); | ||
}); | ||
|
||
test('at the end of a string', () => { | ||
expect(spliceChars('abcd', 4, 0, 'EF')).toBe('abcdEF'); | ||
expect(spliceChars(chalk.green('abcd'), 4, 0, 'EF')).toBe(chalk.green('abcdEF')); | ||
}); | ||
|
||
test('between ansi escape sequences', () => { | ||
expect(spliceChars(chalk.green('ab') + chalk.red('ef'), 2, 0, 'CD')) | ||
.toBe(chalk.green('abCD') + chalk.red('ef')); | ||
expect(spliceChars(chalk.green('ab') + chalk.red('cf'), 3, 0, 'DE')) | ||
.toBe(chalk.green('ab') + chalk.red('cDEf')); | ||
expect(spliceChars(`${chalk.green('a')}b${chalk.red('def')}`, 2, 0, 'C')) | ||
.toBe(`${chalk.green('a')}bC${chalk.red('def')}`); | ||
}); | ||
}); | ||
|
||
describe('deleting characters', () => { | ||
test('from the beginning of a string', () => { | ||
expect(spliceChars('abcdef', 0, 2)).toBe('cdef'); | ||
expect(spliceChars(chalk.green('abcdef'), 0, 2)).toBe(chalk.green('cdef')); | ||
// across escape sequences | ||
expect(spliceChars(chalk.green('abc') + chalk.red('def'), 0, 3)).toBe(chalk.red('def')); | ||
}); | ||
|
||
test('from the middle of a string', () => { | ||
expect(spliceChars('abcdef', 2, 2)).toBe('abef'); | ||
expect(spliceChars(chalk.green('abcdef'), 2, 2)).toBe(chalk.green('abef')); | ||
// accross escape sequences | ||
expect(spliceChars(chalk.green('abc') + chalk.red('def'), 2, 2)) | ||
.toBe(chalk.green('ab') + chalk.red('ef')); | ||
}); | ||
|
||
test('from the end of a string', () => { | ||
expect(spliceChars('abcdef', 4, 2)).toBe('abcd'); | ||
expect(spliceChars(chalk.green('abcdef'), 4, 2)).toBe(chalk.green('abcd')); | ||
// across escape sequences | ||
expect(spliceChars(chalk.green('abcd') + chalk.red('ef'), 4, 2)).toBe(chalk.green('abcd')); | ||
}); | ||
|
||
test('the entire string', () => { | ||
expect(spliceChars('abcdef', 0, 6)).toBe(''); | ||
expect(spliceChars(chalk.green('abcdef'), 0, 6)).toBe(''); | ||
}); | ||
|
||
test('case where deleteCount exceeds the characters in the string', () => { | ||
// the entire string | ||
expect(spliceChars('abcdef', 0, 8)).toBe(''); | ||
// from the second character | ||
expect(spliceChars(chalk.green('abcdef'), 2, 8)).toBe(chalk.green('ab')); | ||
}); | ||
}); | ||
|
||
describe('replacing characters', () => { | ||
test('at the beginning of a string', () => { | ||
expect(spliceChars('abcdef', 0, 3, 'ABC')).toBe('ABCdef'); | ||
expect(spliceChars(chalk.bgRed(chalk.green('abc') + chalk.yellow('def')), 0, 3, 'ABC')) | ||
.toBe(chalk.bgRed(chalk.green('ABC') + chalk.yellow('def'))); | ||
expect(spliceChars(chalk.bgRed(`${chalk.green('ab')}cd${chalk.yellow('ef')}`), 0, 4, 'ABCD')) | ||
.toBe(chalk.bgRed(chalk.green('ABCD') + chalk.yellow('ef'))); | ||
}); | ||
|
||
test('in the middle of a string', () => { | ||
expect(spliceChars('abcdef', 2, 2, 'CD')).toBe('abCDef'); | ||
expect(spliceChars(chalk.green('ab') + chalk.bgRed.yellow('cdef'), 2, 2, 'CD')) | ||
.toBe(chalk.green('abCD') + chalk.bgRed.yellow('ef')); | ||
}); | ||
|
||
test('at the end of a string', () => { | ||
expect(spliceChars('abcdef', 3, 3, 'DEF')).toBe('abcDEF'); | ||
expect(spliceChars(chalk.green('abc') + chalk.bgRed('def'), 3, 3, 'DEF')) | ||
.toBe(chalk.green('abcDEF')); | ||
}); | ||
|
||
test('the entire string', () => { | ||
expect(spliceChars('abcdef', 0, 6, 'ABCDEF')).toBe('ABCDEF'); | ||
expect(spliceChars(chalk.bgRed(chalk.green('abc') + chalk.yellow('def')), 0, 6, 'ABCDEF')) | ||
.toBe(chalk.bgRed.green('ABCDEF')); | ||
}); | ||
}); | ||
}); |