Skip to content

Recipes

m4bwav edited this page Sep 30, 2026 · 1 revision

Each recipe below ran as shown against the published 2.0.0, and the output under it is what it printed.

One regular-expression match

RegExp.prototype.exec gives the match and its index, which is what the function needs. This is the README's example; it replaces the link the expression found and leaves the other one alone:

import replaceStringAtPosition from 'replace-string-at-position';

const text = 'See https://example.com and https://example.com/docs.';
const match = /https:\/\/example\.com\/docs/u.exec(text);
console.log(replaceStringAtPosition(text, match[0], '<link>', match.index));

prints:

See https://example.com and <link>.

exec returns null when nothing matches; check that before reading match.index.

Every match, right to left

Replacing a match with text of another length moves everything after it, so positions found before the first edit are wrong for the later ones. Work from the last match to the first:

import replaceStringAtPosition from 'replace-string-at-position';

const text = 'Call @ana, then @bo.';
const matches = [...text.matchAll(/@[a-z]+/gu)];

let result = text;
for (const match of matches.toReversed()) {
	result = replaceStringAtPosition(result, match[0], `<${match[0].slice(1)}>`, match.index);
}

console.log(result);

let wrong = text;
for (const match of matches) {
	wrong = replaceStringAtPosition(wrong, match[0], `<${match[0].slice(1)}>`, match.index);
}

console.log(wrong);

prints:

Call <ana>, then <bo>.
Call <ana>, then<bo>o.

The second line is what going left to right does: the first edit made the text one unit longer, and the second edit landed one unit early. Array.prototype.toReversed needs Node 20 or later, which the package needs anyway. For a plain "replace every match", String.prototype.replaceAll with a function is simpler; this recipe is for when each edit needs its own position.

Remove a mention

An empty newString deletes. indexOf returns -1 when the text is missing, which the function refuses with a RangeError, so test for it:

import replaceStringAtPosition from 'replace-string-at-position';

const message = 'hi @bot please help';
const mention = '@bot ';
const at = message.indexOf(mention);
if (at !== -1) {
	console.log(replaceStringAtPosition(message, mention, '', at));
}

prints:

hi please help

Replace only when the text is there

The function trusts the position and never compares sourceString with the text there (How text is replaced). When a position might be stale, guard it with startsWith:

import replaceStringAtPosition from 'replace-string-at-position';

function replaceIfPresent(text, expected, replacement, position) {
	if (!text.startsWith(expected, position)) {
		return text;
	}

	return replaceStringAtPosition(text, expected, replacement, position);
}

console.log(replaceIfPresent('hello world', 'world', 'there', 6));
console.log(replaceIfPresent('hello world', 'earth', 'there', 6));

prints:

hello there
hello world

Code point positions

When a position counts code points (characters as [...text] or Array.from sees them) instead of UTF-16 code units, convert it by measuring the code points before it:

import replaceStringAtPosition from 'replace-string-at-position';

const text = '😀😀ab';
const codePointIndex = 2;
const position = [...text].slice(0, codePointIndex).join('').length;
console.log(position);
console.log(replaceStringAtPosition(text, 'a', 'A', position));

prints:

4
😀😀Ab

Grapheme clusters (a flag, an emoji with a skin tone, a letter with combining marks) need Intl.Segmenter for the same conversion; that was not tested here.

Several edits at known positions

Positions measured in the original text stay valid if the edits are applied from the highest position down:

import replaceStringAtPosition from 'replace-string-at-position';

const text = 'The quick fox';
const edits = [
	{position: 0, source: 'The', replacement: 'A'},
	{position: 10, source: 'fox', replacement: 'brown dog'},
];

let result = text;
for (const edit of edits.toSorted((a, b) => b.position - a.position)) {
	result = replaceStringAtPosition(result, edit.source, edit.replacement, edit.position);
}

console.log(result);

prints:

A quick brown dog

Edits that overlap give an answer that depends on their order; keep them apart.

Clone this wiki locally