Skip to content

Commit

Permalink
feat: add spliceChars method
Browse files Browse the repository at this point in the history
  • Loading branch information
luciancooper committed Jun 24, 2021
1 parent 60cdc20 commit 0b58d48
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const stringWidth = require('./stringWidth'),
codePointWidth = require('./codePoint'),
splitLines = require('./splitLines'),
{ sliceChars, sliceColumns } = require('./sliceString'),
spliceChars = require('./spliceChars'),
wordWrap = require('./wordWrap');

module.exports = {
Expand All @@ -18,5 +19,6 @@ module.exports = {
splitLines,
sliceChars,
sliceColumns,
spliceChars,
wordWrap,
};
98 changes: 98 additions & 0 deletions lib/spliceChars.js
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;
};
109 changes: 109 additions & 0 deletions test/spliceChars.test.js
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'));
});
});
});

0 comments on commit 0b58d48

Please sign in to comment.