Here is the solution I use, based on popular xregexp package by Steve Levithan.
Note that left and right params are regx expressions so the solution can handle more difficult scenarios. Solution correctly handles nested parenteses as welll. Refer to http://xregexp.com/api/#matchRecursive for more details and options.
const XRegExp = require('xregexp');
// test: match ${...}
matchRecursive('${a${b}c} d${x} ${e${}${f}g}', '\\${', '}').forEach(match => {
console.log(match.innerText);
});
/* yields:
a${b}c
x
e${}${f}g
*/
interface XRegExpPart { start: number; end: number; name: string; value: string; }
export interface XRegExpMatch { index: number; outerText: string; innerText: string; left: string; right: string; }
export function replaceRecursive(text: string, left: string, right: string, replacer: (match: XRegExpMatch) => string, flags: string = 'g', escapeChar?: string): string {
const matches: XRegExpMatch[] = matchRecursive(text, left, right, flags, escapeChar);
let offset: number = 0;
for (const match of matches) {
const replacement = replacer(match);
if (replacement == match.outerText) continue;
text = replaceAt(text, match.index + offset, match.outerText.length, replacement);
offset += replacement.length - match.outerText.length;
}
return text;
}
export function matchRecursive(text: string, left: string, right: string, flags: string = 'g', escapeChar?: string): XRegExpMatch[] {
// see: https://github.com/slevithan/xregexp#xregexpmatchrecursive
// see: http://xregexp.com/api/#matchRecursive
const parts: XRegExpPart[] = XRegExp.matchRecursive(text, left, right, flags, { valueNames: [null, 'left', 'match', 'right'], escapeChar: escapeChar });
const matches: XRegExpMatch[] = [];
let leftPart: XRegExpPart;
let matchPart: XRegExpPart;
for (const part of parts) {
// note: assumption is made that left, match and right parts occur in this sequence
switch (part.name) {
case 'left':
leftPart = part;
break;
case 'match':
matchPart = part;
break;
case 'right':
matches.push({ index: leftPart!.start, innerText: matchPart!.value, outerText: leftPart!.value + matchPart!.value + part.value, left: leftPart!.value, right: part.value });
break;
default:
throw new Error(`Unexpected part name: '${part.name}'.`);
}
}
return matches;
}
export function replaceAt(string: string, index: number, length: number, replacement: string): string {
return string.substr(0, index) + replacement + string.substr(index + length);
}